frontend/ React 18 + Vite + Tailwind 4 portal (HR self-service, payroll, sales/CRM pipeline)
backend/ Spring Boot 4 / Java 21 API, Flyway migrations, PostgreSQL
docs/ design notes, rulings, API digests
scripts/ backend image push, font check, Thai address import
The frontend talks to the backend through the /api/* contract. Per-app details live in
frontend/README.md and backend/README.md.
Prerequisites: Node ^20.19 or >=22.12, Java 21 (the ./mvnw wrapper is included), and
Docker or a local PostgreSQL 16 for the real-backend path.
Two ways. Pick A to click around in two minutes; pick B to test against the real API.
The whole API runs in the browser from frontend/src/api/mockApi.js, with seeded employees,
leave, overtime, payroll, customers, deals, quotations and a sample price catalogue.
cd frontend
npm install
VITE_USE_MOCKS=true npm run devOpen http://localhost:5174. The login page shows one-click role buttons (ฝ่ายบุคคล, พนักงาน, ผู้จัดการฝ่าย, ฝ่ายขาย, ผู้จัดการฝ่ายขาย, ฝ่ายนำเข้า, ฝ่ายบัญชี, CEO). No password is needed. Changes are held in memory and reset on reload.
The mock catalogue is a small sample (14 products, 25 prices across the 9 real factories). It is enough to exercise the quotation item picker, not to test pricing at full scale. Use path B for that.
1. Start PostgreSQL. The repo's compose file gives you the database the backend expects by
default (hris, user postgres, password postgres, port 5432):
docker compose up -d db2. Start the backend with the demo profile. The profile adds db/migration-demo, which
seeds one login per role plus sample employees, customers and deals. Flyway creates the whole
schema on first boot.
cd backend
SPRING_PROFILES_ACTIVE=demo \
SERVER_SESSION_COOKIE_SECURE=false \
APP_MAIL_PROVIDER=log \
./mvnw spring-boot:runSERVER_SESSION_COOKIE_SECURE=false is required over plain HTTP, otherwise the browser drops the
session cookie and every request after login is a 401. The API listens on http://localhost:8080.
3. Start the frontend (a second terminal):
cd frontend
npm install
npm run devfrontend/.env.development already points the dev server at http://localhost:8080, turns mocks
off, and unlocks the full app. Open http://localhost:5174 and use localhost, not
127.0.0.1. The backend cookie is SameSite=Lax, and the API is on localhost, so a page on
127.0.0.1 never sends it.
4. Quick login. On the dev server the login page shows an เดโม (เซิร์ฟเวอร์จริง) — เข้าสู่ระบบด่วน row with one
button per seeded account. The row exists only under npm run dev with mocks off. It is removed
from production builds. The accounts are demo.<key>@demo.invalid, all with password Demo@2026:
| Button | Login | Use it to test |
|---|---|---|
| พนักงาน | demo.employee@demo.invalid |
self-service, leave, OT, profile requests |
| ฝ่ายบุคคล | demo.hr@demo.invalid |
employees, approvals, payroll, password reset |
| ฝ่ายขาย | demo.sales@demo.invalid |
deals, price requests, quotations |
| ผู้จัดการฝ่ายขาย | demo.salesmanager@demo.invalid |
team deals, commission |
| ฝ่ายนำเข้า | demo.import@demo.invalid |
import requests, catalogue and price import |
| ฝ่ายบัญชี | demo.account@demo.invalid |
deposits, invoices, payments |
| คลังสินค้า | demo.warehouse@demo.invalid |
delivery |
| QC | demo.qc@demo.invalid |
quality checks |
| CEO | demo.ceo@demo.invalid |
price approval, pricing settings, dashboards |
To switch persona, log out and click another button. Restarting the backend also logs you out.
5. Load the price catalogue. This is the one thing the demo seed does not include.
The nine factories exist (Panaria, LEA, Padana, CDE, CITY, REFIN, Equipe, Vives, Bode), but
price_catalog.product_prices is empty, so the product picker in a quotation shows no matches
and no error. The real supplier price lists are not in this repository. Load them one of two ways:
-
Upload through the app. Log in as ฝ่ายนำเข้า or CEO, open นำเข้าราคา (
/price-import), and upload each factory's Excel price list. Import rules per factory are in docs/Catalouge/catalog-import-logic.md. Ask a teammate for the files. -
Copy it from another database. If a teammate has a populated database, dump only the catalogue schema and load it into yours.
size_normandprice_per_sqmare generated columns, so usepg_dump --data-only --column-insertsand run it after step 2 has created the schema:pg_dump --data-only --column-inserts --schema=price_catalog -h <source-host> -U <user> <source-db> \ | psql -h localhost -U postgres hris
Only rows whose price list version is ACTIVE show up in the sales picker.
Reset to a fresh demo. Stop the backend, then recreate the database and start again:
docker compose down -v # drops the pgdata volume
docker compose up -d db| Variable | Effect |
|---|---|
VITE_USE_MOCKS=true |
use the in-browser mock API |
VITE_API_BASE_URL |
API origin the browser calls (.env.development sets http://localhost:8080) |
VITE_API_PROXY_TARGET |
backend the Vite /api proxy forwards to, when VITE_API_BASE_URL is empty |
VITE_SELF_SERVICE_ONLY=true |
show the locked-down production surface (self-service only) to non-HR, non-CEO roles |
VITE_ENABLE_SALES=false |
hide the sales/CRM stack |
If you run the backend on another port or host, add the frontend origin to
APP_CORS_ALLOWED_ORIGINS. The default allows localhost and 127.0.0.1 on ports 5173 and 5174.
Backend (from backend/):
./mvnw test # unit, controller and, when a database is reachable, integration tests
./mvnw -DskipTests package # build the jarThe integration suite resolves its database in this order: TEST_DB_URL, TEST_DB_USERNAME and
TEST_DB_PASSWORD if set; otherwise a Testcontainers postgres:16-alpine when Docker is running;
otherwise those tests skip. With Docker running, a plain ./mvnw test exercises Flyway and the
repositories.
Frontend (from frontend/):
npm run lint # ESLint, including jsx-a11y
npm test # Vitest; run it from frontend/ or source-guard tests fail
npm run buildReal-backend end-to-end tests (Playwright, no mocks) are described in
frontend/e2e-real/README.md. Start the database and backend as above,
then run npm run test:e2e from frontend/.
CI runs backend tests, frontend lint/test/build, and the real-backend e2e suite on every pull
request (.github/workflows/).
This repo includes a root-level vercel.json for Vercel projects whose Root Directory is the repository root. It installs and builds frontend/, publishes frontend/dist, proxies /api/* to the Render backend, and serves index.html for React SPA routes.
Builds are locked to the self-service surface by default (VITE_SELF_SERVICE_ONLY is on unless set to false). The develop branch build sets it to false, so the develop preview shows the full app; every other branch, including main, ships the locked surface.
If the Vercel project Root Directory is set to frontend, the matching frontend/vercel.json can be used instead.
For production/on-prem, run the backend with SPRING_PROFILES_ACTIVE=prod, the target PostgreSQL datasource, and an absolute persistent APP_UPLOADS_DIR path (local disk, SAN/NAS mount, or other backed-up on-prem storage). The demo profile is only for seeded showcase data and must not be enabled against a real production database. Authentication uses employee email from hr.employee; initial passwords should be issued through the HR reset-password flow, not seeded from employee codes.
This is the supported way to give a new employee their first password, and the way to recover an account whose password is lost. It replaced employee-code-derived passwords, which were removed for security in PR #150.
Who can do it: HR only. EmployeeController#resetPassword is gated requireAnyRole(user, "hr"),
so no other role — CEO included — can issue a password.
Steps
- Sign in as HR and go to พนักงานทั้งหมด (
/employees), then open the employee. - In the header, next to แก้ไข, open the ⋯ menu and choose ตั้งรหัสผ่านชั่วคราว.
- Confirm. The employee's existing password stops working immediately.
- The temporary password is displayed. Copy it before closing the dialog and pass it to the employee through a channel you trust.
What to know
- The password is shown exactly once.
EmployeeService#resetPasswordreturns the plaintext a single time and stores only its BCrypt hash, so it cannot be looked up afterwards — not in the UI, not in the database, not in the logs. If it is lost, simply reset again; there is no penalty beyond re-issuing. - The employee is forced to change it at next login. The reset sets
must_change_password = TRUE, so signing in with the temporary password lands them on the change-password screen before they can use the portal. - Every reset is audited as
RESET_EMPLOYEE_PASSWORDagainst the acting HR user. - A reset alone is not enough if the employee has no email on file. The password is written to
hr.employee, and login identity is that row'semail— so an employee with a blank email still cannot sign in. Fill the email in first, then reset.