C-005: Order Acknowledgement Detail + PDF - Order detail page with full line items, releases, addresses, paint codes - PDF export via Puppeteer matching legacy format - Clickable order # links and PDF icons in orders table C-007: BOL Detail + PDF - BOL detail page with ship-from/to, line items, weights - PDF export matching legacy BOL format - PDF icon column in shipments table C-008: Coil Activity - Usage Report - Date range picker (max 31 days, default last 10 days) - Reusable UI: Popover, Calendar (react-day-picker v9), DateRangePicker - SQL from legacy portal_CoilActivityUsage.sql with OnHandQty dedup - 11-column sortable table with search and CSV export C-009: Coil Activity - Receipts Report - VGL customer exception (special SQL vs portal view) - 10-column sortable table with search and CSV export - Shared date range picker component Also: dashboard API route, shipments API route, HDC→HDM mapping, Puppeteer PDF infrastructure, improved error handling. Progress: 9/16 Phase 2 tasks complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
20 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/
│ └── dev-local.yml # Routes dev domain → host:3001 for local dev server
├── 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)/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/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
Dev workflow: Local Next.js dev server with hot reload via PORT=3001 npm run dev. Traefik routes dev.quest.vorteq.wulf.cloud to host:3001 via traefik/config/dev-local.yml. The app-dev Docker container should be stopped during local dev to avoid route conflicts.
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
RSC Serialization — Critical Pattern for Epicor Data
Problem: Next.js RSC (React Server Components) dev mode serializes ALL server-side data and console output to forward to the browser. The mssql package returns recordset objects with circular references, metadata, and prototype chains that blow up RSC serialization (RangeError: Maximum call stack size exceeded at Set.add or Map.set). This is especially severe for large result sets (e.g., ACM has 7,633 shipment rows).
Solution: For Epicor data displayed in the UI, use a client-side fetch → API route pattern instead of RSC server components:
- Create an API route (e.g.,
/api/shipments/route.ts) that calls the service and returnsNextResponse.json(data) - Make the page a
'use client'component that fetches from the API route viauseEffect+fetch() - The API route handles auth/session checks and returns plain JSON — no RSC serialization involved
When RSC works fine: Small result sets (< ~500 rows) through execStoredProc generally work. The [...result.recordset] spread in execStoredProc detaches the array-level prototype. For safety, services should still JSON.parse(JSON.stringify(...)) their return values if passing to RSC.
When RSC breaks: Large result sets (1000+ rows), or any scenario where mssql objects remain in scope during server component rendering. Even with JSON.parse(JSON.stringify()), Next.js dev mode console forwarding can serialize the function's closure scope including mssql objects.
Rule of thumb: If an Epicor query can return > 500 rows for any customer, use the API route pattern.
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
Local Development (Hot Reload)
# Start local dev (instead of Docker container)
docker compose stop app-dev # Release the competing Traefik route
PORT=3001 npm run dev # Run in a tmux pane — hot reload at https://dev.quest.vorteq.wulf.cloud
# Stop local dev (restore containerized version)
# Ctrl+C the dev server, then:
docker compose start app-dev
Traefik routes dev.quest.vorteq.wulf.cloud → http://172.17.0.1:3001 via traefik/config/dev-local.yml. CI/CD pipeline is unchanged — pushes to main still build and deploy the production container.
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)