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>
18 KiB
Vorteq Quest Portal - Claude Code Instructions
Project Overview
Rebuilding the Vorteq Quest customer portal from a legacy Laravel 9 / Windows Server / Savvior stack to a modern Next.js / TypeScript / PostgreSQL stack running in Docker on Linux. The full PRD is in docs/Vorteq_Quest_PRD_v1.0.docx.
Tech Stack
- Runtime: Node.js 22 LTS
- Framework: Next.js 15 with App Router
- Language: TypeScript (strict mode)
- ORM (App DB): Prisma with PostgreSQL 16
- ORM (Epicor):
mssqlpackage (read-only connection to SQL Server) - 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
- Cache/Queue: Redis 7 via BullMQ
- Email: Microsoft Graph API (Office 365)
- PDF: Puppeteer or @react-pdf/renderer
- Testing: Vitest (unit/integration), Playwright (E2E)
- Containers: Docker + Docker Compose
- Reverse Proxy: Traefik v3 with Cloudflare DNS challenge for SSL
- 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.
Infrastructure
Server
- Host: expvtcasp01 (Linux/Ubuntu)
- Netbird IP: 100.89.62.20
- Stack directory:
/opt/stacks/vorteq - Runner directory:
/opt/stacks/forgejo-runner
Repository
- URL:
forgejo.wulfconsulting.cloud/lorentz/quest-vorteq - SSH:
ssh://git@forgejo.wulfconsulting.cloud:222/lorentz/quest-vorteq.git(port 222) - Registry:
forgejo.wulfconsulting.cloud/lorentz/quest-vorteq
Domains
- Production:
quest.vorteq.wulf.cloud - Development:
dev.quest.vorteq.wulf.cloud - Testing:
testing.vorteq.wulf.cloud - Traefik Dashboard:
traefik.vorteq.wulf.cloud
Running Services
- Traefik: Reverse proxy, auto-SSL via Cloudflare DNS challenge
- PostgreSQL 16: Three databases —
vorteq_dev,vorteq_test,vorteq_prod - Redis 7: Three DB numbers — 0 (dev), 1 (test), 2 (prod)
- Forgejo Runner: Docker-in-Docker, registered as
expvtcasp01
CI/CD Pipeline
Push to main → build Docker image → tag as :latest → push to Forgejo registry → deploy to prod
Push to development → build Docker image → tag as :dev → push to Forgejo registry → deploy to dev
Project Structure
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 (node:22-alpine, standalone output)
├── .forgejo/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline
├── traefik/
│ ├── traefik.yml # Traefik configuration
│ ├── acme.json # SSL certificates (gitignored)
│ └── config/ # Dynamic Traefik config
├── prisma/
│ ├── 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/
│ │ ├── (portal)/ # Authenticated route group
│ │ │ ├── dashboard/
│ │ │ ├── inventory/ # Summary + 6 category pages + detail drill-down
│ │ │ ├── orders/
│ │ │ └── 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 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 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 # 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/ # 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
- TypeScript strict mode, no
anytypes - Use
typeoverinterfaceunless extending - Prefer
constoverlet, nevervar - Use named exports, not default exports (except page.tsx and layout.tsx)
- Error handling: use typed error classes, not string throws
- All API responses:
{ data: T }on success,{ error: string, code: string }on failure
Naming
- Files: kebab-case (
shipment-requests.ts) - Components: PascalCase (
ShipmentRequestForm.tsx) - Functions/variables: camelCase
- Database columns (Prisma): snake_case
- 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 - 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
Component Pattern
// Server component by default
export async function InventorySummary({ category }: { category: string }) {
const data = await getInventorySummary(category);
return <DataTable columns={columns} data={data} />;
}
// Client component only when interactivity needed
'use client';
export function ShipmentRequestCart() { ... }
Epicor Connection Patterns
Connection (src/lib/epicor.ts)
- Singleton connection pool via
mssqlpackage - Typed error classes:
EpicorConnectionError,EpicorQueryError,EpicorTimeoutError execStoredProc<T>(name, params)— execute stored procedure, returnsTexecQuery<T>(query, params)— execute parameterized SQL, returnsTexecPortalStoredProc<T>(name, custId, dbName, sub, additionalParams)— convenience wrapper with standard portal paramsgetPortalDbName()— returns[VorteqPortal](bracketed DB name for cross-DB SP parameter)checkConnection()— health check- Graceful shutdown on SIGINT/SIGTERM
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,@WAREHOUSEfor 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
execQuerywith parameterized SQL against Epicor views - Standard view:
dbo.portal_Orders WHERE CustomerID = @CustID - HDC exception: uses
dbo.portal_OrdersHDCview and maps CustIDHDC→HDM
Column Mapping
Epicor SP results use PascalCase column names. Services map them to snake_case:
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_accounttables - Email + password enabled, email verification disabled
- 7-day session expiry
Quest Session (src/lib/permissions.ts)
getQuestSession() enriches the Better Auth session with:
questUserId— fromquest_usertableactiveCompanyId— from cookie-based session store (src/lib/session.ts)isSubUser— fromquest_user.is_sub_userpermissionRules[]— collected from user type → permission groups → permission rulesuserType— 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 permissionhasPermission(rule)— returns booleanisAdmin()/requireAdmin()— check for Admin or Super Admin user typeisSubUser()— check sub-user statusrequireActiveCompany()— throws if no company selectedgetActiveCompany()— returns the activequest_companyrecordgetUserCompanies()— 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 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 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. 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
# 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
# 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>
# PostgreSQL (per-environment, set in docker-compose.yml)
DATABASE_URL=postgresql://postgres:<password>@postgres:5432/vorteq_dev
# Redis (per-environment, set in docker-compose.yml)
REDIS_URL=redis://redis:6379/0
# Epicor SQL Server (read-only)
MSSQL_HOST=wi-e10test
MSSQL_DATABASE=Epicor10Live
MSSQL_USER=portal
MSSQL_PASSWORD=<from-env>
MSSQL_PORT=1433
# Portal database name (for cross-DB stored procedure parameter)
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-env>
MAIL_FROM=quest@vorteqcoil.com
# reCAPTCHA
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>
Testing Requirements
- Unit tests for all service functions (inventory, orders, shipments data transformations)
- Integration tests for API routes with mocked DB connections
- E2E tests for: login flow, inventory browse + drill-down, shipment request workflow, invoice upload
- Data validation: compare Epicor query results with known-good outputs from the legacy system
Git Workflow
mainbranch: production-ready code → deploys toquest.vorteq.wulf.clouddevelopmentbranch: integration branch → deploys todev.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 testbefore committing - Push triggers Forgejo Actions → Docker build → registry push → deploy
Task Tracking
All tasks are tracked in TASKS.md. Mark tasks as:
[ ]Not started[~]In progress[x]Complete[!]Blocked (note blocker in comments)