- 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>
bg-purple (formula, AI badges) was only defined in company-settings-lookups.css
which is not loaded on Quote/Job pages. The badge element rendered with no
background, appearing as a blank space in front of the item description.
Moved all 13 custom color utilities (purple, pink, cyan, teal, indigo, lime,
brown, gray, orange, yellow, green, blue, red) to site.css so they are
available globally. company-settings-lookups.css retains its definitions for
now (harmless duplication; can be cleaned up later).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The UNIQUE index on (CompanyId, Email) uses HasFilter([Email] IS NOT NULL),
so NULL allows multiple rows but empty string '' does not — every blank-email
customer after the first was hitting a duplicate-key violation at save time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New logic:
Tier 1 - email present: email match -> skip (unchanged)
Tier 2 - email absent + phone present: name + phone composite -> skip
Tier 3 - email and phone absent: name + city/state/zip composite -> warn, import anyway
Tier 2 requires BOTH name and phone to match so two people sharing an
office line don't falsely collide. Tier 3 warns but imports because
location data is too imprecise to hard-skip on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tier 1 (email): existing behavior, now uses HashSet instead of O(n²) .Any()
Tier 2 (phone): when email is absent, deduplicate by normalised phone number
(last 10 digits of MobilePhone then Phone) against both DB and within-batch
Tier 3 (name): when both email and phone are absent, warn but still import
Fixes customers with no email being silently skipped or left undetected as
duplicates. NormalizePhone strips formatting so (423) 331-9834 and
423-331-9834 match correctly.
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>
Migration was scaffolded in the background while the dev server held the PDB lock
and silently failed to write the file. Re-ran and applied now.
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>
Merging the kiosk PIN script and the role-permissions script into a single
@section Scripts block fixes the InvalidOperationException thrown when Razor
encounters two sections with the same name in the same view.
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>
JobImportDto was missing Description despite Job entity having the field.
Downloadable template now includes a Description column; the importer maps
it directly to Job.Description with a fallback chain of Description ->
SpecialInstructions -> "Imported job".
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>
Subscription expiry (SubscriptionExpiryBackgroundService):
- Trials with no grace period now go directly Active -> Expired instead
of briefly entering GracePeriod for a day, which was causing repeated
'Grace Period Started' admin notification emails
- Remove redundant isTrial variable (query already filters to non-Stripe
companies, so all processed companies are trials by definition)
- Save per-company inside the loop so a single SaveChangesAsync failure
no longer discards all other companies' status changes and notification
log entries (which was the other cause of repeated emails)
HTML entities in page titles (33 views):
- Replace – / — with plain ' - ' in ViewData["Title"] C#
strings; Razor HTML-encodes these when rendering @ViewData["Title"],
causing browsers to display the literal text '–' instead of a dash
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>
The #settingsTabs <button> elements had no explicit background-color,
letting browser UA button styling (white) bleed through in dark mode.
Added transparent overrides so the dark body background shows instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NCalc2 operates on double internally; passing decimal parameters caused
'Operator * cannot be applied to double and decimal' at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 7-step guided walkthrough modal (concept, output modes, fields, formula,
testing, box example, cylinder example); auto-shown first time the tab
opens with no templates; always accessible via "How it works" button
- Variable pill badges below formula input showing all valid field names + rate;
update live as fields are added/renamed; clickable to insert at cursor
- Fix: Add Field no longer shows validation error on blank new rows; validation
only fires once the user has typed something
- Help article: added Common Formula Patterns section with box, cylinder, and
flat panel worked examples (fields table + formula + expected output)
- HelpKnowledgeBase updated with pattern examples and walkthrough note
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>
- HelpKnowledgeBase.cs: QR label now opens a preview modal (not new tab); mention list-page QR button; add vendor supply categories knowledge
- Help/Inventory.cshtml: Update QR printing steps for modal workflow; document list-page QR button
- Help/Vendors.cshtml: Add Supply Categories section with filtering behaviour and fallback; add nav link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
X-Frame-Options: DENY blocked all iframe embeds including our own QR label
modal. Changed to SAMEORIGIN and added frame-ancestors 'self' to CSP so
same-origin iframes (Label page) load correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
frame-src was missing 'self' so the Label iframe was blocked by CSP.
bootstrap.Modal.getOrCreate does not exist; correct method is getOrCreateInstance.
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>
Opens the same iframe modal used on the Details page; the iframe src is
set dynamically so a single shared modal handles every row.
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>
Razor's @() expression auto-encodes &, turning — into &mdash; which
rendered as literal text in the browser. Wrapped all such expressions in
@Html.Raw() so the em-dash entity is passed through unescaped.
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>
Manufacturer product pages are often not on secure connections; http:// is the
safer default to avoid connection failures on non-SSL sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If a stored URL is missing http:// or https:// the browser treats it as relative
and appends it to the app URL. Guard in three places:
- inventory-catalog-lookup.js syncLinkButton: ensureAbsoluteUrl() prepends https://
- inventory-label-scan.js syncLink: same guard for scan-filled URL fields
- Details.cshtml SafeUrl() Razor helper on SpecPageUrl, SdsUrl, TdsUrl links
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>
aspnetcore-browser-refresh.js uses plain ws:// (not wss://) so it was
blocked by the CSP which only listed wss://localhost:*. Both are needed
in dev: ws:// for the dotnet watch browser refresh socket, wss:// for
SignalR.
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>
sw.js: Remove fetch event interception entirely. The passthrough
e.respondWith(fetch(request)) call was throwing TypeError on localhost
HTTPS due to certificate trust differences in the SW context, causing
JS/CSS resource loads to fail. The SW exists for PWA installability
only — no interception is needed to satisfy that requirement.
inline-item-edit.js: Move window.inlineItemEdit config read inside
DOMContentLoaded so script load order vs. config assignment in
@section Scripts doesn't matter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The script IIFE was reading window.inlineItemEdit at load time, before
the inline <script> block in @section Scripts had executed to set it.
Config read moved inside DOMContentLoaded so it fires after all inline
scripts in the section have run, regardless of src vs. inline order.
cfg is now passed as a parameter to makeEditable and attachListeners
instead of being captured from the outer IIFE scope.
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>
Replace hardcoded #fff / #f8f9fa / #dee2e6 / #e8eeff colors with Bootstrap
CSS variables so the dropdown respects the active theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>