feat: replace company switcher button with searchable combobox
Some checks failed
Build and Deploy / build (push) Successful in 3m26s
Build and Deploy / deploy (push) Failing after 2s

Add cmdk-based combobox to portal header for inline company switching
with search by name or Epicor ID. Also update select-company page to
use the same pattern. Fix admin company switching by allowing admins
to switch to any active company without requiring a quest_user_company
link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lorentz 2026-02-16 18:07:39 +00:00
parent 87c1cdf8da
commit 56ce4ba940
10 changed files with 747 additions and 185 deletions

319
CLAUDE.md
View file

@ -11,7 +11,7 @@ Rebuilding the Vorteq Quest customer portal from a legacy Laravel 9 / Windows Se
- **Language:** TypeScript (strict mode)
- **ORM (App DB):** Prisma with PostgreSQL 16
- **ORM (Epicor):** `mssql` package (read-only connection to SQL Server)
- **Auth:** Better Auth with credentials provider
- **Auth:** Better Auth with credentials provider + custom session enrichment
- **UI:** shadcn/ui + Tailwind CSS
- **Data Tables:** TanStack Table v8
- **State:** React Server Components by default, client components only when needed
@ -24,23 +24,19 @@ Rebuilding the Vorteq Quest customer portal from a legacy Laravel 9 / Windows Se
- **CI/CD:** Forgejo Actions with self-hosted runner
- **Registry:** Forgejo built-in OCI container registry
## Reference Documentation\
\
All legacy system documentation is in the `docs/` directory:\
\
- **`vorteqcoil-portal-documentation.md`** — Complete interface documentation of the legacy portal: navigation, sections, column layouts, business logic. Reference when building UI pages.\
- **`vorteqcoil-visual-documentation.html`** — Visual reference guide with color coding system and UI patterns. Reference for design decisions.\
- **`VorteqDiscovery_20260206_205453.txt`** — Server infrastructure discovery: IIS config, scheduled tasks, services.\
- **`VorteqAppDiscovery_20260206_210519.txt`** — Application code discovery: Laravel routes, controllers, models, views, Blade templates. Reference when implementing features.\
- **`VorteqEpicorDB_20260210_211850.txt`** — Epicor database schema: tables, stored procedures, views. Reference when writing Epicor queries.\
- **`EpicorDatabase.php`** — Legacy PHP database class with all Epicor query implementations. Primary reference for porting queries to TypeScript.\
- **`_FILE_LISTING.txt`** — Complete file listing of the legacy Laravel application.\
- **`_MODEL_MAP.txt`** — Legacy Eloquent model-to-table mappings. Reference for Prisma schema design.\
- **`_SQL_QUERIES_FOUND.txt`** — All SQL queries extracted from the legacy codebase. Reference for service layer implementation.\
\
' CLAUDE.md
## Reference Documentation
All legacy system documentation is in the `docs/` directory:
- **`vorteqcoil-portal-documentation.md`** — Complete interface documentation of the legacy portal: navigation, sections, column layouts, business logic. Reference when building UI pages.
- **`vorteqcoil-visual-documentation.html`** — Visual reference guide with color coding system and UI patterns. Reference for design decisions.
- **`VorteqDiscovery_20260206_205453.txt`** — Server infrastructure discovery: IIS config, scheduled tasks, services.
- **`VorteqAppDiscovery_20260206_210519.txt`** — Application code discovery: Laravel routes, controllers, models, views, Blade templates. Reference when implementing features.
- **`VorteqEpicorDB_20260210_211850.txt`** — Epicor database schema: tables, stored procedures, views. Reference when writing Epicor queries.
- **`EpicorDatabase.php`** — Legacy PHP database class with all Epicor query implementations. Primary reference for porting queries to TypeScript.
- **`_FILE_LISTING.txt`** — Complete file listing of the legacy Laravel application.
- **`_MODEL_MAP.txt`** — Legacy Eloquent model-to-table mappings. Reference for Prisma schema design.
- **`_SQL_QUERIES_FOUND.txt`** — All SQL queries extracted from the legacy codebase. Reference for service layer implementation.
## Infrastructure
@ -78,7 +74,7 @@ quest-vorteq/
├── CLAUDE.md # This file
├── TASKS.md # Task tracking
├── docker-compose.yml # Infrastructure: traefik, postgres, redis, app containers
├── Dockerfile # Multi-stage Next.js build
├── Dockerfile # Multi-stage Next.js build (node:22-alpine, standalone output)
├── .forgejo/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline
@ -87,67 +83,97 @@ quest-vorteq/
│ ├── acme.json # SSL certificates (gitignored)
│ └── config/ # Dynamic Traefik config
├── prisma/
│ ├── schema.prisma # PostgreSQL schema
│ ├── schema.prisma # PostgreSQL schema (auth + quest + ship/alloc/finance/paint/doc/wave/lts domains)
│ ├── migrations/
│ └── seed.ts # Seed data / migration scripts
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # Auth route group (login, forgot-password, account-request)
│ │ ├── (auth)/ # Auth route group
│ │ │ ├── login/
│ │ │ ├── forgot-password/
│ │ │ └── account-request/
│ │ ├── (portal)/ # Authenticated route group
│ │ │ ├── dashboard/
│ │ │ ├── inventory/
│ │ │ ├── inventory/ # Summary + 6 category pages + detail drill-down
│ │ │ ├── orders/
│ │ │ ├── shipments/
│ │ │ ├── coil-activity/
│ │ │ ├── invoices/
│ │ │ ├── shipment-requests/
│ │ │ ├── allocation-requests/
│ │ │ ├── jobs/
│ │ │ └── admin/ # Admin-only pages
│ │ ├── api/ # API routes
│ │ └── layout.tsx
│ │ │ └── select-company/
│ │ ├── api/
│ │ │ ├── auth/ # Better Auth catch-all + custom login + set-active-company
│ │ │ ├── health/
│ │ │ └── account-request/
│ │ ├── layout.tsx
│ │ ├── globals.css
│ │ └── page.tsx
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── data-table/ # Reusable TanStack Table wrapper
│ │ ├── layout/ # Navigation, header, sidebar
│ │ └── forms/ # Shared form components
│ │ ├── ui/ # shadcn/ui primitives (button, card, input, table, toast, etc.)
│ │ ├── dashboard/ # summary-card, quick-nav-card, recent-orders-table, recent-shipments-table
│ │ ├── inventory/ # inventory-summary-table, inventory-detail-table
│ │ ├── orders/ # orders-table
│ │ ├── layout/ # portal-sidebar, portal-header, breadcrumb
│ │ └── company-selector.tsx
│ ├── hooks/
│ │ └── use-toast.ts
│ ├── lib/
│ │ ├── db.ts # Prisma client singleton
│ │ ├── epicor.ts # MSSQL connection + query helpers
│ │ ├── auth.ts # Better Auth configuration
│ │ ├── email.ts # Microsoft Graph email service
│ │ ├── pdf.ts # PDF generation service
│ │ ├── permissions.ts # RBAC helpers
│ │ └── utils.ts # General utilities
│ │ ├── epicor.ts # MSSQL connection pool + query helpers + typed errors
│ │ ├── auth.ts # Better Auth config (Prisma adapter, credentials provider)
│ │ ├── auth-client.ts # Better Auth client-side helpers
│ │ ├── session.ts # Quest-specific session data (cookie-based)
│ │ ├── permissions.ts # RBAC: getQuestSession, requirePermission, hasPermission, isAdmin, isSubUser, getActiveCompany, getUserCompanies, PermissionRules constants
│ │ └── utils.ts # General utilities (cn, etc.)
│ ├── services/
│ │ ├── inventory.ts # Inventory data access (calls Epicor SPs)
│ │ ├── orders.ts # Order data access
│ │ ├── shipments.ts # Shipment data access
│ │ ├── coil-activity.ts # Coil activity data access
│ │ ├── ship-requests.ts # Shipment request business logic
│ │ ├── alloc-requests.ts # Allocation request business logic
│ │ └── invoices.ts # Invoice processing
│ ├── jobs/
│ │ ├── worker.ts # BullMQ worker setup
│ │ ├── shipment-nag.ts
│ │ ├── order-ack-email.ts
│ │ ├── invoice-cleanup.ts
│ │ ├── shipping-reports.ts
│ │ └── wave-edi.ts
│ └── types/
│ ├── epicor.ts # Epicor query result types
│ ├── portal.ts # Portal-specific types
│ └── index.ts
│ │ ├── inventory.ts # Epicor SP calls for 6 inventory categories (summary + detail)
│ │ ├── orders.ts # portal_Orders view queries, HDC/HDM exception handling
│ │ └── dashboard.ts # Dashboard data aggregation
│ ├── types/
│ │ ├── epicor.ts # Epicor query result types
│ │ ├── portal.ts # Portal-specific types
│ │ └── index.ts
│ └── middleware.ts # Rate limiting, public route allowlist, auth check (currently disabled in dev)
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── scripts/
│ └── migrate-data.ts # SQL Server → PostgreSQL migration
└── docs/
└── Vorteq_Quest_PRD_v1.0.docx
└── docs/ # Legacy system documentation (see Reference Documentation above)
```
### Not Yet Created (Planned)
```
src/app/(portal)/shipments/
src/app/(portal)/coil-activity/
src/app/(portal)/invoices/
src/app/(portal)/shipment-requests/
src/app/(portal)/allocation-requests/
src/app/(portal)/jobs/
src/app/(portal)/admin/
src/services/shipments.ts # Shipment data access
src/services/coil-activity.ts # Coil activity data access
src/services/ship-requests.ts # Shipment request business logic
src/services/alloc-requests.ts # Allocation request business logic
src/services/invoices.ts # Invoice processing
src/lib/email.ts # Microsoft Graph email service
src/lib/pdf.ts # PDF generation service
src/jobs/ # BullMQ workers (shipment-nag, order-ack-email, etc.)
src/components/data-table/ # Reusable TanStack Table wrapper
src/components/forms/ # Shared form components
```
## Current Development State
**Phase 1 (Foundation):** Complete — project scaffold, Docker infra, Prisma schema (all domains), Epicor connection, Better Auth, middleware, permissions, company selector.
**Phase 2 (Core Features):** In progress
- Dashboard: implemented (summary cards, quick nav, recent orders/shipments tables)
- Inventory: implemented (all 6 category summaries + detail drill-down)
- Orders: implemented (list page with search and CSV export, HDC exception)
- Remaining: shipments, coil-activity, invoices, shipment-requests, allocation-requests, jobs, admin pages
**Auth status:** Better Auth is configured with Prisma adapter. A custom login endpoint exists at `/api/auth/login`. Auth middleware is temporarily disabled in dev mode (`DEV_MODE=true` bypasses to a mock admin session). The dev domain (`dev.quest.vorteq.wulf.cloud`) also bypasses auth in middleware.
**Git:** Currently only `main` branch. No `development` branch yet.
## Coding Conventions
### General
@ -166,46 +192,18 @@ quest-vorteq/
- API routes: kebab-case (`/api/shipment-requests`)
- Environment variables: SCREAMING_SNAKE_CASE
### Formatting (Prettier)
- Single quotes, semicolons, trailing commas (es5)
- 2-space indentation, 80 char print width
- Tailwind class sorting via `prettier-plugin-tailwindcss`
### Database
- Prisma schema uses snake_case for all fields
- Map legacy PascalCase column names with `@map("LegacyName")` where needed for migration clarity
- Map legacy PascalCase column names with `@map("LegacyName")` where needed
- Always use parameterized queries for Epicor MSSQL — never string interpolation
- Epicor queries go in `src/services/` with typed return values
- PostgreSQL queries go through Prisma client exclusively
### Epicor Connection Pattern
```typescript
// src/lib/epicor.ts
import sql from 'mssql';
const epicorPool = new sql.ConnectionPool({
server: process.env.MSSQL_HOST!,
database: process.env.MSSQL_DATABASE!,
user: process.env.MSSQL_USER!,
password: process.env.MSSQL_PASSWORD!,
options: { encrypt: false, trustServerCertificate: true },
});
// Always use parameterized queries
export async function execStoredProc(name: string, params: Record<string, unknown>) {
const pool = await epicorPool.connect();
const request = pool.request();
for (const [key, value] of Object.entries(params)) {
request.input(key, value);
}
return request.execute(name);
}
```
### Inventory Query Pattern
The Epicor stored procedures require three standard parameters:
- `CustID`: The selected company's EpicorCustID
- `DBNAME`: The VorteqPortal database name wrapped in brackets, e.g., `[VorteqPortal]`
- `sub`: 0 or 1, indicating if the current user is a sub-user (IsSubUser flag)
V6 stored procedures (WIP, Finished Goods, Processed Other) use the `sub` parameter.
Older procedures (Unprocessed, R&R variants) block sub-users entirely — return empty array if `isSubUser`.
### Component Pattern
```typescript
// Server component by default
@ -219,56 +217,131 @@ export async function InventorySummary({ category }: { category: string }) {
export function ShipmentRequestCart() { ... }
```
### Auth Pattern (Better Auth)
```typescript
// src/lib/auth.ts
import { betterAuth } from 'better-auth';
## Epicor Connection Patterns
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
database: {
type: 'postgres',
url: process.env.DATABASE_URL,
},
// Credentials provider: validate against auth_user table (bcrypt)
// Session includes: userId, questUserId, activeCompanyId, isSubUser, permissionRules[]
});
```
### Connection (`src/lib/epicor.ts`)
- Singleton connection pool via `mssql` package
- Typed error classes: `EpicorConnectionError`, `EpicorQueryError`, `EpicorTimeoutError`
- `execStoredProc<T>(name, params)` — execute stored procedure, returns `T`
- `execQuery<T>(query, params)` — execute parameterized SQL, returns `T`
- `execPortalStoredProc<T>(name, custId, dbName, sub, additionalParams)` — convenience wrapper with standard portal params
- `getPortalDbName()` — returns `[VorteqPortal]` (bracketed DB name for cross-DB SP parameter)
- `checkConnection()` — health check
- Graceful shutdown on SIGINT/SIGTERM
### Middleware Pattern
### Inventory Stored Procedure Parameters
**V6 procedures** (WIP, Finished Goods, Processed Other):
- `@Customer` — the selected company's EpicorCustID
- `@DBNAME` — portal database name in brackets, e.g., `[VorteqPortal]`
- `@SUBUSER` — 0 or 1 (sub-user flag)
- Detail variants also accept `@PART`, `@PLANT`, `@WAREHOUSE` for filtering
**Non-V6 procedures** (Unprocessed, R&R variants):
- `@CUSTID` — the selected company's EpicorCustID
- `@DBNAME` — portal database name in brackets
- No sub-user parameter — sub-users are blocked entirely (return empty array)
- Detail variants accept `@part`, `@plant`, `@warehouse` (lowercase)
### Orders Pattern
- Uses `execQuery` with parameterized SQL against Epicor views
- Standard view: `dbo.portal_Orders WHERE CustomerID = @CustID`
- HDC exception: uses `dbo.portal_OrdersHDC` view and maps CustID `HDC``HDM`
### Column Mapping
Epicor SP results use PascalCase column names. Services map them to snake_case:
```typescript
// src/middleware.ts - protect all (portal) routes
// src/lib/permissions.ts - check specific permission rules
export function requirePermission(rule: string) {
// Check user's permission groups contain the rule
function mapSummaryRow(raw: Record<string, unknown>): InventorySummaryRow {
return {
part_num: String(raw.VorteqPartNum ?? ''),
description: String(raw.VorteqPartDesc ?? ''),
on_hand_qty: Number(raw.OnHandQty ?? 0),
// ...
};
}
```
## Auth & Session Pattern
### Better Auth (`src/lib/auth.ts`)
- Prisma adapter connecting to `auth_user`, `auth_session`, `auth_account` tables
- Email + password enabled, email verification disabled
- 7-day session expiry
### Quest Session (`src/lib/permissions.ts`)
`getQuestSession()` enriches the Better Auth session with:
- `questUserId` — from `quest_user` table
- `activeCompanyId` — from cookie-based session store (`src/lib/session.ts`)
- `isSubUser` — from `quest_user.is_sub_user`
- `permissionRules[]` — collected from user type → permission groups → permission rules
- `userType` — e.g., "Admin", "Super Admin", "Customer User"
In dev mode (`DEV_MODE=true`), returns a mock admin session with all permissions.
### Permission Helpers (`src/lib/permissions.ts`)
- `requirePermission(rule)` — throws if user lacks permission
- `hasPermission(rule)` — returns boolean
- `isAdmin()` / `requireAdmin()` — check for Admin or Super Admin user type
- `isSubUser()` — check sub-user status
- `requireActiveCompany()` — throws if no company selected
- `getActiveCompany()` — returns the active `quest_company` record
- `getUserCompanies()` — returns accessible companies (admins see all active)
- `PermissionRules` — constant object with all permission rule names
## Important Business Logic
### Company Context
After login, the user's active company is stored in the session. All Epicor queries are scoped to that company's `EpicorCustID`. Admin users can switch companies.
After login, the user's active company is stored in a cookie (`quest_session`). All Epicor queries are scoped to that company's `EpicorCustID`. Admin users can switch companies and see all active companies.
### Sub-User Restrictions
Users with `IsSubUser=true` on their `quest_User` record:
- See filtered inventory data (sub=1 parameter to V6 stored procedures)
Users with `is_sub_user=true` on their `quest_user` record:
- See filtered inventory data (SUBUSER=1 parameter to V6 stored procedures)
- See filtered ship-to addresses (only those matching '%NB HANDY%')
- Cannot access Unprocessed inventory or R&R data at all
### HDC/HDM Customer Exception
Customer 'HDC' uses a special order view (`portal_OrdersHDC`) and maps to CustID 'HDM' in queries. This needs to be handled in the order service.
Customer 'HDC' uses a special order view (`portal_OrdersHDC`) and maps to CustID 'HDM' in queries. Handled in `src/services/orders.ts`.
### VGL Customer Exception
Customer 'VGL' uses a special SQL query for coil receipts instead of the standard view.
## Commands
```bash
# Development
npm run dev # Start Next.js dev server
npm run build # Production build
npm run start # Start production server
npm run lint # ESLint
npm run typecheck # TypeScript type checking (tsc --noEmit)
npm test # Vitest
npm run test:e2e # Playwright E2E tests
# Database
npx prisma migrate dev # Create + apply migration (dev)
npx prisma migrate deploy # Apply migrations (prod)
npx prisma db push # Push schema without migration
npx prisma generate # Regenerate Prisma client
npx prisma studio # Database GUI
tsx prisma/seed.ts # Run seed script
# Docker
docker compose up -d # Start all services
docker compose ps # Check service status
docker compose logs -f vorteq-dev # Tail dev container logs
# Pre-commit checks
npm run lint && npm run typecheck && npm test
```
## Environment Variables
```env
# Application
NODE_ENV=development|test|production
DEV_MODE=true # Bypasses auth, uses mock admin session
BETTER_AUTH_URL=https://dev.quest.vorteq.wulf.cloud
BETTER_AUTH_SECRET=<generated-by-setup-script>
BETTER_AUTH_SECRET=<generated>
# PostgreSQL (per-environment, set in docker-compose.yml)
DATABASE_URL=postgresql://postgres:<password>@postgres:5432/vorteq_dev
@ -280,7 +353,7 @@ REDIS_URL=redis://redis:6379/0
MSSQL_HOST=wi-e10test
MSSQL_DATABASE=Epicor10Live
MSSQL_USER=portal
MSSQL_PASSWORD=<from-current-env>
MSSQL_PASSWORD=<from-env>
MSSQL_PORT=1433
# Portal database name (for cross-DB stored procedure parameter)
@ -289,12 +362,12 @@ PORTAL_DB_NAME=VorteqPortal
# Microsoft Graph (Email)
MS_GRAPH_CLIENT_ID=629eca61-d554-4763-b14a-565aee7b7ab8
MS_GRAPH_TENANT_ID=291e541e-003c-463c-8834-863b9e7c1ebe
MS_GRAPH_CLIENT_SECRET=<from-current-env>
MS_GRAPH_CLIENT_SECRET=<from-env>
MAIL_FROM=quest@vorteqcoil.com
# reCAPTCHA
RECAPTCHA_SITE_KEY=<from-current-env>
RECAPTCHA_SECRET_KEY=<from-current-env>
RECAPTCHA_SITE_KEY=<from-env>
RECAPTCHA_SECRET_KEY=<from-env>
# Cloudflare (Traefik SSL - set in .env at stack level)
CLOUDFLARE_DNS_API_TOKEN=<from-cloudflare>
@ -310,7 +383,7 @@ CLOUDFLARE_DNS_API_TOKEN=<from-cloudflare>
## Git Workflow
- `main` branch: production-ready code → deploys to `quest.vorteq.wulf.cloud`
- `development` branch: integration branch → deploys to `dev.quest.vorteq.wulf.cloud`
- `development` branch: integration branch → deploys to `dev.quest.vorteq.wulf.cloud` (not yet created)
- Feature branches: `feature/F-001-project-scaffold`
- Commit messages: `feat(F-001): scaffold Next.js project with TypeScript + Tailwind`
- Run `npm run lint && npm run typecheck && npm test` before committing