Commit Graph

23 Commits

Author SHA1 Message Date
spouliot ed35362c7a Add Formula Library ratings, Job Profitability report, and Quote Revision History improvements
- 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>
2026-06-01 09:02:07 -04:00
spouliot ca7e905832 Add Community Formula Library feature
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>
2026-05-27 21:54:51 -04:00
spouliot 97745f9a65 Add Timeclock settings tab in Company Settings with multi-kiosk support
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>
2026-05-27 00:12:46 -04:00
spouliot 6c2fe6e1c4 Add Employee Timeclock feature with kiosk, attendance report, and payroll CSV export
- 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>
2026-05-26 19:53:13 -04:00
spouliot 1eba50cf0f Add Custom Formula Item Templates with AI generation and wizard integration
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>
2026-05-23 15:09:22 -04:00
spouliot d77b3778ac Add vendor supply categories with inventory auto-filter
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>
2026-05-23 09:52:34 -04:00
spouliot 8acbc8605d Harden multi-tenant isolation across all user-facing controllers
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>
2026-05-17 18:04:22 -04:00
spouliot 1a44133a63 Remove ShopWorker entity and migrate worker identity to ApplicationUser
Removes the ShopWorker and ShopWorkerRoleCost entities, all related DTOs,
mappings, controllers, views, and import/export paths. Worker identity is
now handled entirely through ApplicationUser with per-user LaborCostPerHour.
ShopWorkerRoleCosts table remains in production pending manual data migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 20:32:32 -04:00
spouliot 6a918c2afc Add invoice SMS notifications and customer intake kiosk
Invoice SMS:
- Send Invoice modal now prompts Email/SMS/Both based on customer contact data
- New /invoice/{token} customer-facing view page with full line items and pay button
- PublicViewToken (permanent) added to Invoice; separate from expiring PaymentLinkToken
- InvoiceSent SMS default template added; customizable via Notification Templates settings
- {{viewUrl}} placeholder documented in template editor

Customer Intake Kiosk:
- Tablet kiosk flow: Contact → Job → Terms/Signature → Confirmation
- Remote link mode for off-site customers (lighter form, no signature)
- KioskHub (AllowAnonymous SignalR) for staff-to-tablet push without login
- Staff activates tablet via cookie; sends remote link manually
- Submitted sessions create Customer + Job automatically; fires in-app notification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:25:27 -04:00
spouliot 4fd9c52aaf Phase G: Add Budgeting and Year-End Close
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>
2026-05-10 13:01:56 -04:00
spouliot fde24b09c9 Phase F: Add Invoice Write-Off, Fixed Assets, Period Locking, and 1099 Tracking
- 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>
2026-05-10 12:19:32 -04:00
spouliot d94612cc9c Fix 4 post-review issues found in accounting module audit
- 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>
2026-05-10 11:32:44 -04:00
spouliot 42eff3357e Phase G: Add Recurring Transactions (BackgroundService + CRUD UI)
- RecurringTemplate entity with Frequency/IntervalCount/NextFireDate/EndDate/MaxOccurrences/TemplateData JSON
- RecurringFrequency + RecurringTemplateType enums
- RecurringTransactionService BackgroundService: hourly check, creates Draft bills or immediate expenses, advances NextFireDate, auto-deactivates on limits
- RecurringTemplatesController: Index/Create/Edit/ToggleActive/Delete/GenerateNow (on-demand fire)
- Three views + external JS for type-toggle and dynamic bill line items
- Finance sidebar nav: Recurring Transactions
- Migration: AddRecurringTemplates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 11:08:36 -04:00
spouliot d3a5d827f9 Phase F: Customer/Vendor Statements, Payment Terms Parser, Tax Rates
F1: GetCustomerStatementAsync/GetVendorStatementAsync on IFinancialReportService;
    StatementLineDto; CustomerStatementDto/VendorStatementDto; Statement action on
    CustomersController + VendorsController; Statement views + PDF download via
    StatementPdfHelper (QuestPDF); Statement button on Customer/Vendor Details pages.

F2: PaymentTermsParser static helper (CalculateDueDate, ParseEarlyPaymentDiscount);
    EarlyPaymentDiscountPercent/Days on Invoice entity; GetCustomerPaymentTerms AJAX
    endpoint on InvoicesController auto-populates Terms + due date on customer select;
    early payment discount notice on Invoice Create.

F3: TaxRate entity (Name/Rate/State/IsDefault/IsActive, tenant-filtered);
    IUnitOfWork.TaxRates + UnitOfWork + ApplicationDbContext; TaxRatesController
    (Index/Create/Edit/Delete/ToggleActive, CompanyAdminOnly); GetTaxRateForCustomer
    AJAX endpoint; Tax Rates in Settings gear menu.

Also fixes AddVendorCredits migration: VendorCreditApplications FKs changed from
CASCADE to NoAction to resolve SQL Server error 1785 (multiple cascade paths).
Migration: AddPaymentTermsAndTaxRates applied locally; 200/200 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 10:55:22 -04:00
spouliot 1229081436 Phase E: Add Bank Reconciliation
- IsCleared + ClearedDate added to Payment, BillPayment, Expense entities
- BankReconciliation entity (account, statement date, beginning/ending balance, status)
- BankReconciliationStatus enum (InProgress, Completed)
- Migration AddBankReconciliation: new BankReconciliations table + IsCleared/ClearedDate columns
- IUnitOfWork/UnitOfWork wired with BankReconciliations repo
- BankReconciliationsController: Index, Create, Reconcile, ToggleCleared (AJAX), Complete, Report
- Reconcile view: deposit/payment checkboxes with live running balance and difference via JS
- Complete is gated: only enabled when difference == $0.00
- Nav: Bank Reconciliation added to Finance section in _Layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:10:38 -04:00
spouliot cf9dcfb4c1 Phase D: Add Vendor Credits (AP cycle completion)
- 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>
2026-05-10 00:03:14 -04:00
spouliot a33687f7bd Phase C: Add Manual Journal Entries (double-entry GL)
- 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>
2026-05-09 23:56:03 -04:00
spouliot 713efbc2b6 Store Data Protection keys in SQL Server (non-production)
Replaces the local filesystem path (which required IIS app pool write
access to inetpub\wwwroot\DataProtection-Keys) with SQL Server storage
via IDataProtectionKeyContext. Keys now survive deploys and IIS recycles
without any server-side folder permission setup.

Production continues to use Azure Blob Storage unchanged.

- Add Microsoft.AspNetCore.DataProtection.EntityFrameworkCore 8.0.11 to
  Web and Infrastructure projects
- ApplicationDbContext implements IDataProtectionKeyContext
- Migration AddDataProtectionKeys creates DataProtectionKeys table
- Program.cs: non-production path uses PersistKeysToDbContext

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 22:08:31 -04:00
spouliot 1fc79b77fe Add platform powder catalog, catalog-first lookup, and label scanner
- Platform PowderCatalogItem table (IPlainRepository, no tenant filter) with
  full spec fields: cure temp/time, finish, color families, clear coat flag,
  coverage sq ft/lb, transfer efficiency, IsUserContributed
- Two EF migrations: AddPowderCatalogItem + AddPowderCatalogSpecFields
- PowderCatalogController (SuperAdminOnly): import from Prismatic JSON scrape,
  Lookup AJAX endpoint (catalog-first, ranked by SKU exact match), stats view
  with Tenant Contributed card
- Unified smart Lookup button on inventory Create/Edit: catalog hit fills all
  fields via catalogSnapshot pattern; AI augments cure/finish data from product
  URL if subscription enabled; catalog miss falls through to AI lookup
- In-browser label scanner (_LabelScanModal): getUserMedia live camera feed,
  jsQR auto-detects QR codes in rAF loop; "Scan Label Text" fallback sends
  captured frame to Claude vision via /Inventory/ScanLabel
- ScanLabel endpoint handles both QR URL path (LookupByUrlAsync) and vision
  path (ScanLabelAsync); auto-inserts unrecognized products as
  IsUserContributed=true; returns wasInCatalog/addedToCatalog flags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 16:36:25 -04:00
spouliot 6569d9c4ea Add SMS gating, TCPA terms agreement, and compose-before-send modal
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in
- CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version
- SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion)
- Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers
- Removed redundant Ready for Pickup SMS (Job Completed covers it)
- Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends
- Send SMS button on job details for ad-hoc messages (Admin/Manager only)
- SendJobSmsAsync auto-appends STOP opt-out language if missing
- Migrations: AddSmsGating, AddCompanySmsAgreement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 22:29:39 -04:00
spouliot 54f444d981 Add AI Catalog Price Check feature
Claude reviews every active catalog item against the shop's own operating costs
and returns a per-item verdict (below-cost / thin-margin / high / ok) with a
suggested price range, cost floor, and assumptions.

- New entity: CatalogPriceCheckReport (JSON blob, archived per company)
- New service: IAiCatalogPriceCheckService / AiCatalogPriceCheckService
  batches items 25 at a time to stay within model context limits
- Two new controller actions: GET AiPriceCheck (view report) + POST RunAiPriceCheck
- AiPriceCheck view: summary cards (counts by verdict), color-coded item cards
  with Edit Price link, assumptions detail, and loading spinner on submit
- AI Price Check button added to catalog Index header
- Migration AddCatalogPriceCheckReport applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:41:56 -04:00
spouliot 0bb96a502a Add passkey / biometric login (WebAuthn FIDO2)
Shop floor workers can log in once with a password, enroll a passkey,
and use Face ID / Windows Hello / fingerprint for all future logins.

- UserPasskey entity + AddUserPasskeys migration (Fido2 v4.0.1)
- PasskeyController: RegisterOptions, Register, LoginOptions, Login,
  Manage, Remove endpoints
- Login page: platform-aware button (Face ID / Windows Hello / etc.)
  hidden automatically if browser doesn't support WebAuthn
- Post-login floating prompt to enroll on first use; session-dismissed
- Passkeys & Biometrics link in user dropdown menu
- Manage page: list registered devices, add new, remove individual
- passkey.js: targeted base64url conversion (only challenge + user.id
  + credential IDs) — fixes "Required parameters missing" error caused
  by blindly converting rp.id and other string fields to ArrayBuffers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:07:01 -04:00
spouliot 63e12a9636 Initial commit 2026-04-23 21:38:24 -04:00