When multiple jobs need the same powder, the 'Powder in Queue to be
Ordered' panel now collapses them into a single line (summed lbs) rather
than showing one row per coat. 'Mark as Ordered' marks all contributing
coats at once and injects each into the 'Awaiting Receipt' panel
individually so per-coat receiving still works unchanged.
- Add PowderOrderJobRefDto; PowderOrderLineDto gains CoatIds + Jobs lists
(scalar CoatId/JobId/etc. become computed accessors for backward compat)
- MapPowderOrderGroupsMerged: secondary GroupBy on (ColorName, ColorCode,
Finish, SKU) within vendor group for the 'needed' panel
- MapPowderOrderGroups kept per-coat for the 'awaiting receipt' panel
- MarkPowderOrdered accepts comma-separated coatIds, returns coats array
- Dashboard view: Customer column loops job refs for merged rows; JS posts
coatIds and iterates data.coats to populate awaiting-receipt panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Guidance for Claude Code when working in this repository.
## Project Overview
A production-ready ASP.NET Core 8.0 MVC application for managing powder coating business operations. The application implements Clean Architecture with six projects across three layers (Domain, Application, Infrastructure) plus two presentation layers (Web MVC, RESTful API).
ASP.NET Core 8.0 MVC application for powder coating business operations. Clean Architecture:
Company Admin (seed): demo@powdercoatinglogix.com / CompanyAdmin123!
```
## Architecture Overview
## Architecture
### Clean Architecture Layers
### Layers
- **Core** — Entities, enums, repository + service interfaces. `BaseEntity` provides `Id`, `CompanyId`, `CreatedAt`, `UpdatedAt`, `IsDeleted`, audit fields on every entity.
- **Application** — DTOs, AutoMapper profiles (auto-discovered via `cfg.AddMaps()`; `PricingTierProfile` is an exception — registered manually in `Program.cs`), service interfaces. No UI/infra deps.
- **Infrastructure** — `ApplicationDbContext`, `Repository<T>`, `UnitOfWork`. Seed data is **manual only** via Platform Management → Seed Data.
-`SaveChangesAsync()` or `CompleteAsync()` to persist changes
**Dependency Injection**
- All dependencies registered in `Program.cs`
- Controllers inject `IUnitOfWork` and `IMapper`
- Services are scoped to request lifetime
**Global Query Filters**
- Soft deletes: All queries automatically filter `IsDeleted == false`
- Multi-tenancy: Non-SuperAdmin users see only their company data
- Bypass with `ignoreQueryFilters: true` parameter in repository methods
### Multi-Tenancy Implementation
-`CompanyId` foreign key on all business entities
-`ITenantContext` injected into DbContext resolves current company
- SuperAdmin role can view all companies
- Global query filters enforce company isolation at database level
- Users have both system role (SuperAdmin) and company role (CompanyAdmin, Manager, Worker, Viewer)
**Critical:** global filters are not sufficient on their own. Every `FindAsync`/`GetAllAsync` in a controller must also include an explicit `CompanyId == currentCompanyId` predicate — defense in depth.
## Data Access Rules (ENFORCE THESE)
> **`ApplicationDbContext` is NEVER injected into a controller.**
> All data access in controllers goes through `IUnitOfWork`. No exceptions outside the list below.
> **This rule is enforced at startup:** `EnforceDataAccessArchitecture()` in `Program.cs` scans all
> controllers at boot and throws if any non-exempt controller injects `ApplicationDbContext`.
> Full rationale and permanent exceptions list: `docs/DATA_ACCESS_ARCHITECTURE.md`
> All data access goes through `IUnitOfWork`. Enforced at startup by `EnforceDataAccessArchitecture()` in `Program.cs`.
> Full rationale + permanent exceptions: `docs/DATA_ACCESS_ARCHITECTURE.md`
**AI uses Anthropic Claude Sonnet 4.6** (`claude-sonnet-4-6`) — NOT OpenAI. The `AI:Anthropic:ApiKey` config key is what the AI photo quoting and AI scheduling services read.
- **Announcements** — platform-wide announcements to tenants
### Key Pricing Rules
- Custom powder (no inventory item + `PowderToOrder` > 0): charge for the **full ordered quantity**, not just calculated usage
- In-stock inventory powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
- Tax exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
### Key Rules
- Custom powder (no inventory item + `PowderToOrder > 0`): charge for the **full ordered quantity**
- In-stock powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
- Tax-exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote/invoice create; marked ★ in dropdowns
### Pricing Routing Flags — Must Stay In Sync Across All Three Layers
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes each item to the correct pricing path using boolean flags. **These flags MUST exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, AND be mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes via boolean flags. **Must exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
| Flag | Effect if missing on JobItem |
|------|------------------------------|
@@ -489,83 +138,77 @@ All modules below are fully implemented with controllers, views, and migrations
| `IsLaborItem` | Item repriced at surface-area rate instead of hours × labor rate |
| `IsSalesItem` | ManualUnitPrice ignored; item repriced using coat/surface math |
**Checklist when adding a new pricing routing flag:**
1. Add the property to `QuoteItem` (Core/Entities)
2. Add the property to `JobItem` (Core/Entities)
3. Add it to `CreateQuoteItemDto` (Application/DTOs)
4. Add it to `JobItemSeed` (private class in JobItemAssemblyService)
5. Map it in all three `JobItemAssemblyService.CreateJobItem` overloads
6. Include it in every `existingItemsData` JSON block in job views (`Edit.cshtml`, `EditItems.cshtml`) and in all job controller actions that build `CreateQuoteItemDto` from a `JobItem`
7. Add a migration if the field is new on a persisted entity
8.The structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` in `JobItemAssemblyServiceTests` will fail until steps 1–3 are done — this is intentional
**Checklist when adding a new flag:**
1. Add to `QuoteItem` (Core/Entities)
2. Add to `JobItem` (Core/Entities)
3. Add to `CreateQuoteItemDto` (Application/DTOs)
4. Add to `JobItemSeed` (private class in `JobItemAssemblyService`)
5. Map in all three `JobItemAssemblyService.CreateJobItem` overloads
6. Include in every `existingItemsData` JSON block in `Edit.cshtml`, `EditItems.cshtml`, and all controller actions that build `CreateQuoteItemDto` from a `JobItem`
7. Add migration if field is new on a persisted entity
8.Structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` fails until steps 1–3 are done — intentional
### Branding
- Application name: **Powder Coating Logix**
- PCL logo: `wwwroot/images/pcl-logo.png` — used in sidebar header (when no tenant logo), login/register pages, sidebar footer
- Sidebar footer always shows PCL logo linking to `http://www.powdercoatinglogix.com`
- Tenant companies can upload their own logo (stored in Azure Blob `companylogos` container); it replaces the PCL logo in the sidebar header
---
## Known Issues
## Configuration
- Entity Framework warnings about global query filters on related entities (non-critical, informational only)
Limits in `AppConstants.cs`: 10 MB max, allowed: jpg/jpeg/png/gif/pdf/doc/docx/xls/xlsx.
## Testing Strategy
---
- **Unit Tests**: Test business logic in isolation
- **Integration Tests**: Test full request pipeline with test database
- Use xUnit framework
- Mock `IUnitOfWork` in unit tests
## UI Rules
## Extending the System
- **HTML entities in `.cshtml`** — `—` not `—`, `×` not `×`, `…` not `…`. Literal Unicode gets corrupted by AI tools + Windows file encoding.
- **External JS files only** — put scripts in `wwwroot/js/*.js`, reference via `src=`. Inline `@section Scripts` blocks can silently fail with SyntaxErrors from layout HTML context.
- **`alert-permanent` CSS class** — `_Layout` auto-dismisses `.alert:not(.alert-permanent)` after ~5s. Any non-toast alert that must persist needs this class.
AI uses Anthropic Claude Sonnet 4.6 via `IAiQuoteService`. Configure the key under `AI:Anthropic:ApiKey` in `appsettings.json`.
1. Create service interface in `Application/Interfaces/`
2. Implement in `Infrastructure/Services/` calling the Anthropic client
3. Inject into controllers via DI
## Gotchas
### SignalR Hubs
- **Two data export controllers**: `DataExportController` (SuperAdmin) and `AccountDataExportController` (company self-service). When changing CSV columns, fix **both**.
- **Help docs**: when a feature changes, update both `HelpKnowledgeBase.cs` (AI assistant knowledge) and the matching article in `Views/Help/` (human-readable help center).
- **Demo reset**: `DemoController.ResetDemoData` is gated on `company.CompanyCode == "DEMO"` — only the demo tenant can trigger a reset. ForceRemoveAll wipes all company data before reseeding.
- **artemis@ account**: the "break glass" root SuperAdmin — guards in `PlatformUsersController` protecting it are intentional, never remove them.
Two hubs are already implemented and mapped in `Program.cs`:
A visual redesign is in progress. If the user asks about UI changes, dashboard/jobs/board styling, or the new design tokens, read `design_handoff_pcl_redesign/README.md` and follow `design_handoff_pcl_redesign/CLAUDE.md` for that work.
**Other**: Help Center (14 articles at `Views/Help/`) · Setup Wizard (10-step, `SetupWizardController`) · Reports (24 actions: P&L, AR Aging, Powder Usage, Cycle Time, PDF exports) · Gift Certificates · Announcements · In-App Notification Bell · Passkey/Biometric Login (WebAuthn, Fido2NetLib) · Customer Intake Kiosk (iPad, SignalR push, `KioskSession`) · AI Accounting Features (receipt scan, AR follow-up, smart categorization, cash flow forecast, anomaly detection)
- Sidebar footer always links to `http://www.powdercoatinglogix.com`
- Tenant logos: Azure Blob `companylogos` container; replaces PCL logo in sidebar header only
## Active Design Work
A visual redesign is in progress. For UI changes, dashboard/jobs/board styling, or design tokens: read `design_handoff_pcl_redesign/README.md` and follow `design_handoff_pcl_redesign/CLAUDE.md`.
alert(data.message || 'Could not update. Please try again.');
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.