Imported from adrian-danciu/corpex-erp (
AGENTS.md). Install upstream withnpx skills add adrian-danciu/corpex-erp. Copyright stays with the author.
AGENTS.md — Corpex ERP
Concise orientation for Codex sessions. Goal: get productive on this repo in under 5 minutes.
What this project is
Corpex — bachelor-thesis ERP web app for Romanian SMEs. Integrated platform covering: Stock & warehouse, HR (employees + leave flow), Fleet (vehicles + ITP/RCA/CASCO/Rovinieta tracking), Finance (partners + invoices + payments), Projects (cross-cutting client jobs), Payroll, Notifications, and Reporting/Dashboard.
Product requirements live in:
- docs/CORPEX - Plan lucrare de licenta.pdf — thesis plan
- docs/CORPEX - Prezentare module.pdf — module spec
Tech stack
- Runtime / package manager: Bun (everywhere, never npm/yarn).
- Frontend (apps/web): React 19 + Vite 7 + TypeScript, Apollo Client 4, Zustand, React Hook Form + Zod, shadcn/ui (Radix + Tailwind 4), Lucide icons, recharts, react-d3-tree, @react-pdf/renderer, react-router-dom 7.
- Backend (apps/api): NestJS 11 + GraphQL (code-first, schema auto-generated to apps/api/src/schema.gql) + Apollo Server 5, Prisma 7 + Postgres (Neon serverless via
@prisma/adapter-pg), JWT auth (Passport), bcrypt, multer for uploads. - Monorepo: lightweight — root
package.jsonis mostly empty, each app is independent. No workspace tooling.
Repo layout (high-level)
corpex-erp/
├── apps/
│ ├── api/ # NestJS GraphQL backend
│ │ ├── src/
│ │ │ ├── auth/ # JWT + Department-based RBAC (permissions.config.ts)
│ │ │ ├── users/ # User CRUD (ADMIN-managed)
│ │ │ ├── employees/ # HR: Employees + LeaveRequests + Approvals
│ │ │ ├── finance/ # Partners, Invoices, Payments
│ │ │ ├── stock/ # Warehouses, Products, StockMovements, PurchaseOrders (+ ledger/receiving helpers)
│ │ │ ├── fleet/ # Vehicles, Documents (expiry), Mileage, Leases, Expenses
│ │ │ ├── projects/ # Cross-cutting: Members, Materials, Vehicles, Tasks, Feed, cost rollup
│ │ │ ├── reporting/ # Dashboard aggregation queries
│ │ │ ├── settings/ # CompanySettings singleton
│ │ │ ├── payroll/ # Payroll periods, lines, RO tax calculations
│ │ │ ├── notifications/ # In-app notifications + expiry schedulers
│ │ │ ├── common/ # Pagination DTOs/helpers
│ │ │ ├── prisma/ # PrismaModule
│ │ │ ├── schema.gql # AUTOGENERATED — never edit by hand
│ │ │ ├── app.module.ts
│ │ │ └── main.ts # CORS, /uploads static, listen on PORT or 3000
│ │ ├── prisma/
│ │ │ ├── schema.prisma
│ │ │ ├── migrations/
│ │ │ └── seed.ts # Run via `bunx prisma db seed`
│ │ ├── prisma.config.ts
│ │ └── uploads/ # gitignored, project-feed attachments
│ └── web/
│ ├── src/
│ │ ├── components/
│ │ │ ├── ui/ # shadcn primitives (see UI rule below)
│ │ │ ├── auth/ # LoginForm, ProtectedRoute
│ │ │ ├── layout/ # Layout, DashboardLayout
│ │ │ ├── common/ # Pagination etc.
│ │ │ ├── dashboard/ # Widgets
│ │ │ ├── fleet/, projects/ # module-specific
│ │ ├── pages/{hr,finance,stock,fleet,projects}/
│ │ ├── graphql/
│ │ │ ├── mutations/ # Apollo mutation docs + legacy mixed docs
│ │ │ ├── queries/ # Migrated query docs
│ │ │ └── fragments/ # Shared GraphQL fragments
│ │ ├── hooks/ # Shared hooks: pagination, URL filters, disclosure, mutation toast
│ │ ├── lib/
│ │ │ ├── apollo-client.ts # JWT auth link
│ │ │ ├── permissions.ts # Frontend mirror of department permissions
│ │ │ ├── formatters.ts # Shared money/date/quantity/bytes formatting
│ │ │ ├── download.ts # Shared blob/URL downloads
│ │ │ └── schemas/ # Zod schemas
│ │ ├── stores/auth.store.ts # Zustand + localStorage persist
│ │ └── App.tsx # Router with ProtectedRoute(requiredModule, requiredAccess)
│ └── vite.config.ts
└── docs/ # *.en.md / *.ro.md, plus PDFs and superpowers plans
Module status (what's wired)
All feature NestJS modules are registered in apps/api/src/app.module.ts and have working GraphQL resolvers + Prisma persistence. Frontend has matching pages routed in apps/web/src/App.tsx.
| Module | Backend | Frontend | Notes |
|---|---|---|---|
| Auth | ✅ | ✅ Login | JWT 15m access + 7d refresh; tokens in localStorage |
| Users / Accounts | ✅ | ✅ Admin list + employee-driven generation | IT/HR can generate accounts from employee records; temporary passwords require first-login change |
| HR | ✅ | ✅ Employees, Leave, Approvals, Org chart, Documents | Org chart uses react-d3-tree; employee docs support expiry notifications |
| Finance | ✅ | ✅ Overview, Partners, Invoices (with project cost import) | PDF render via @react-pdf/renderer |
| Stock | ✅ | ✅ Overview, Warehouses, Products, Movements, Purchase Orders | reservedQty, defectiveQty, in-transit PO quantities, and NIR receipts |
| Fleet | ✅ | ✅ List, Create, Detail (5 tabs) | expiringDocuments(daysAhead) query + dashboard widget |
| Projects | ✅ | ✅ List, Create, Detail (7 tabs incl. kanban) | Project-scoped RBAC via @RequireProjectAccess |
| Reporting | ✅ | ✅ Reports + Dashboard | Aggregated metrics |
| Settings | ✅ | ✅ Settings page | CompanySettings is a single singleton row |
| Payroll | ✅ | ✅ Payroll page | Draft -> Approved -> Paid lifecycle, RO tax rules, B2B contractor handling |
| Notifications | ✅ | ✅ Bell/inbox | Stock, leave, task and document expiry notifications |
Known gaps vs. the PDF spec
- Audit log: not implemented as a dedicated cross-module audit trail.
- e-Factura integration: explicitly listed as "future" in the thesis.
- Heavy lazy chunks: route-level code-splitting is implemented and the app entry is below Vite's 500 kB warning line. PDF and XLSX exports are split by action-level dynamic imports;
@react-pdf/rendererstill creates a large lazypdf-exportchunk. Visualization vendors are split by purpose (charts-vendor,org-chart-vendor,kanban-vendor) inapps/web/vite.config.ts.
Key conventions
Backend module pattern (mandatory)
Every feature module follows this layout: entities/ (GraphQL ObjectTypes) → dto/ (Input types, paginated wrappers) → *.service.ts (Prisma calls) → *.resolver.ts (decorators + guards) → registered in *.module.ts → imported in AppModule. Look at apps/api/src/fleet/ for a clean reference.
For Nest constructor injection, injectable providers must be imported as runtime values. Do not use import type for services, guards, strategies, resolvers, or modules that appear in constructors/providers; Nest needs the class token at runtime.
Auth + permissions (department-based, NOT just role)
User.roleis onlyUSER/ADMIN—ADMINis the platform-superuser.- Real access control is via
Employee.department(HR, FINANCE, WAREHOUSE, FLEET, MANAGEMENT, IT). Mapping table in apps/api/src/auth/permissions.config.ts definesread/write/noneper module per department, plusleaveApprovalsboolean andreportsscope. - Resolvers guard with
@UseGuards(JwtAuthGuard, DepartmentGuard)+@RequireModule('moduleKey', 'read'|'write'). - Frontend mirrors this with
<ProtectedRoute requiredModule="..." requiredAccess="...">. - Projects has its own per-project RBAC:
@RequireProjectAccess('member' | 'manager')resolves project context fromprojectId/taskId/memberId/ etc.
Employee-driven account generation
- Preferred account creation flow: create the
Employeefirst, then generate the linkedUserfrom the employee record. - Email format is generated from the normalized name under
@corpex.com(ana.smith@corpex.com; collisions becomeana.smith2@corpex.com). - Temporary password format follows the generated email local part plus current year (
ana.smith.2026) and setsUser.mustChangePassword = true. User.roleremainsUSERfor generated employee accounts; effective permissions still come fromEmployee.department. UseADMINonly for platform administrators.- IT has read access to HR records so it can bulk-generate accounts from the Employees table. HR/Management/Admin can also generate where permitted by HR access.
- First login redirects users with
mustChangePasswordto/change-password; successfulchangePasswordclears the flag.
UI primitive rule (non-negotiable)
- ALWAYS use shadcn primitives from apps/web/src/components/ui/. Currently present:
badge,button,card,checkbox,confirmation-dialog,dialog,input,label,page-loading,pagination,popover,select,separator,sheet,sonner,spinner,table,tabs,textarea,tooltip. - Do NOT hand-roll
<table>,<button>,<input>etc. with raw Tailwind. - If you need a primitive that isn't in
ui/(e.g.dropdown-menu,combobox): STOP and tell the user to add it via the shadcn CLI — do not invent a substitute. - Same rule when refactoring: replace hand-rolled primitives with the shadcn equivalent.
Forms
- React Hook Form + Zod via
zodResolver. - Schemas live in apps/web/src/lib/schemas/ and re-export from
index.ts. - Numeric inputs use
valueAsNumber: true. - For watched values used in render logic, prefer
useWatch({ control, name })over directwatch("field")calls. - Exception: LoginForm.tsx uses plain
useState(legacy — intentional, see docs/auth_implementation.md).
State
- Zustand only for shared/navigation-persistent state (
auth.store.ts). No Redux. - URL-backed filters use
useUrlFilters. - Dialog/workflow state should stay local or move into feature-local controller hooks (
useVehicleDetailController,usePayrollController,useMaterialAllocation) before considering a global store.
GraphQL on the web side
- New or migrated queries should live under apps/web/src/graphql/queries/.
- New shared selections should live under apps/web/src/graphql/fragments/.
- Mutations live under apps/web/src/graphql/mutations/.
- Some legacy
*.mutations.tsfiles still hold both queries and mutations. Keep compatibility exports when migrating one module at a time instead of rewriting every import at once.
Commands you'll run constantly
Fresh-machine setup (after git clone)
# Root
bun install
# API: env + Prisma
cd apps/api
# create .env with DATABASE_URL=... and JWT_SECRET=...
bun install
bunx prisma generate
bunx prisma migrate deploy
bunx prisma db seed # optional
# Web: env
cd ../web
# create .env with VITE_API_URL=http://localhost:3000/graphql
bun install
Daily dev
# Backend (port 3000, Playground at /graphql)
cd apps/api && bun run start:dev
# Frontend (port 5173)
cd apps/web && bun run dev
After editing prisma/schema.prisma
cd apps/api
bunx prisma migrate dev --name <descriptive_name>
bunx prisma generate
Type-check / lint
- API:
bun run typecheck,bun run lint,bun run test - Web:
bun run typecheck,bun run lint,bun run build
Environment variables
apps/api/.env (gitignored — must create on every new machine):
DATABASE_URL="postgresql://USER:PASS@HOST:5432/DB?schema=public"
JWT_SECRET="<64-byte hex; generate with: node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\">"
PORT=3000
CORS_ORIGIN=http://localhost:5173
apps/web/.env:
VITE_API_URL=http://localhost:3000/graphql
Deeper docs
When more detail is needed, the human-curated docs are in docs/ (each in EN + RO):
- docs/README.en.md
- docs/structure.en.md
- docs/architecture.en.md — module breakdown, projects/fleet architecture deep-dive
- docs/libraries.en.md
- docs/scripts.en.md
- docs/validation.en.md
- docs/auth_implementation.md — JWT flow + role usage
- docs/superpowers/plans/ — historical and current implementation plans, including the codebase optimization pass
When in doubt
- Pick a similar working module (fleet for backend pattern, VehiclesPage.tsx for a list page) and mirror its shape.
- Run
bun run typecheckin the affected app before declaring done. - The
schema.gqlis auto-regenerated on backend start — never edit it manually.