-
First step of pbc-finance GL growth: the chart of accounts. Backend: - Account entity (code, name, accountType: ASSET/LIABILITY/EQUITY/ REVENUE/EXPENSE, description, active) - AccountJpaRepository + AccountService (list, findById, findByCode, create with duplicate-code guard) - AccountController at /api/v1/finance/accounts (GET list, GET by id, POST create). Permission-gated: finance.account.read, .create. - Liquibase 003-finance-accounts.xml: table + unique code index + 6 seeded accounts (1000 Cash, 1100 AR, 1200 Inventory, 2100 AP, 4100 Sales Revenue, 5100 COGS) - finance.yml updated: Account entity + 2 permissions + menu entry SPA: - AccountsPage with sortable list + inline create form - finance.listAccounts + finance.createAccount in typed API client - Sidebar: "Chart of Accounts" above "Journal Entries" in Finance - Route /accounts wired in App.tsx + SpaController + SecurityConfig This is the foundation for the next step (JournalEntryLine child entity with per-account debit/credit legs + balanced-entry validation). The seeded chart covers the 6 accounts the existing event subscribers will reference once the double-entry lines land. -
Two more operator-facing forms: - CreateLocationPage: code, name, type (WAREHOUSE/BIN/VIRTUAL) - AdjustStockPage: item dropdown, location dropdown, absolute quantity. Creates the balance row if absent; sets it to the given value if present. Shows the resulting balance inline. API client: inventory.createLocation, inventory.adjustBalance. Locations list gets "+ New Location"; Balances list gets "Adjust Stock". Routes wired at /locations/new and /balances/adjust. With this commit, every PBC entity that operators need to create or manage has a SPA form: items, partners, locations, stock balances, sales orders, purchase orders, work orders (with BOM + routing), users, and roles. The only create-less entities are journal entries (read-only, event-driven) and stock movements (append-only ledger). -
Closes the R2 gap: an admin can now manage users and roles entirely from the SPA without touching curl or Swagger UI. Backend (pbc-identity): - New RoleService with createRole, assignRole, revokeRole, findUserRoleCodes, listRoles. Each method validates existence + idempotency (duplicate assignment rejected, missing role rejected). - New RoleController at /api/v1/identity/roles (CRUD) + /api/v1/identity/users/{userId}/roles/{roleCode} (POST assign, DELETE revoke). All permission-gated: identity.role.read, identity.role.create, identity.role.assign. - identity.yml updated: added identity.role.create permission. SPA (web/): - UsersPage — list with username link to detail, "+ New User" - CreateUserPage — username, display name, email form - UserDetailPage — shows user info + role toggle list. Each role has an Assign/Revoke button that takes effect on the user's next login (JWT carries roles from login time). - RolesPage — list with inline create form (code + name) - Sidebar gains "System" section with Users + Roles links - API client + types: identity.listUsers, getUser, createUser, listRoles, createRole, getUserRoles, assignRole, revokeRole Infrastructure: - SpaController: added /users/** and /roles/** forwarding - SecurityConfiguration: added /users/** and /roles/** to the SPA permitAll block -
Extends the R1 SPA with create forms for the four entities operators interact with most. Each page follows the same pattern proven by CreateSalesOrderPage: a card-scoped form with dropdowns populated from the API, inline validation, and a redirect to the detail or list page on success. New pages: - CreateItemPage — code, name, type (GOOD/SERVICE/DIGITAL), UoM dropdown populated from /api/v1/catalog/uoms - CreatePartnerPage — code, name, type (CUSTOMER/SUPPLIER/BOTH), optional email + phone - CreatePurchaseOrderPage — symmetric to CreateSalesOrderPage; supplier dropdown filtered to SUPPLIER/BOTH partners, optional expected date, dynamic line items - CreateWorkOrderPage — output item + quantity + optional due date, dynamic BOM inputs (item + qty/unit + source location dropdown), dynamic routing operations (op code + work center + std minutes). The most complex form in the SPA — matches the EBC-PP-001 work order creation flow API client additions: catalog.createItem, partners.create, purchaseOrders.create, production.createWorkOrder — each a typed wrapper around POST to the corresponding endpoint. List pages updated: Items, Partners, Purchase Orders, Work Orders all now show a "+ New" button in the PageHeader that links to the create form. Routes wired: /items/new, /partners/new, /purchase-orders/new, /work-orders/new — all covered by the existing SpaController wildcard patterns and SecurityConfiguration permitAll rules. -
Reworks the demo seed and SPA to match the reference customer's work-order management process (EBC-PP-001 from raw/ docs). Demo seed (DemoSeedRunner): - 7 printing-specific items: paper stock, 4-color ink, CTP plates, lamination film, business cards, brochures, posters - 4 partners: 2 customers (Wucai Advertising, Globe Marketing), 2 suppliers (Huazhong Paper, InkPro Industries) - 2 warehouses with opening stock for all items - Pre-seeded WO-PRINT-0001 with full BOM (3 inputs: paper + ink + CTP plates from WH-RAW) and 3-step routing (CTP plate-making @ CTP-ROOM-01 -> offset printing @ PRESS-A -> post-press finishing @ BIND-01) matching EBC-PP-001 steps C-010/C-040 - 2 DRAFT sales orders: SO-2026-0001 (100x business cards + 500x brochures, $1950), SO-2026-0002 (200x posters, $760) - 1 DRAFT purchase order: PO-2026-0001 (10000x paper + 50kg ink, $2550) from Huazhong Paper SPA additions: - New CreateSalesOrderPage with customer dropdown, item selector, dynamic line add/remove, quantity + price inputs. Navigates to the detail page on creation. - "+ New Order" button on the SalesOrdersPage header - Dashboard "Try the demo" section rewritten to walk the EBC-PP-001 flow: create SO -> confirm (auto-spawns WOs) -> walk WO routing -> complete (material issue + production receipt) -> ship SO (stock debit + AR settle) - salesOrders.create() added to the typed API client The key demo beat: confirming SO-2026-0001 auto-spawns WO-FROM-SO-2026-0001-L1 and -L2 via SalesOrderConfirmedSubscriber (EBC-PP-001 step B-010). The pre-seeded WO-PRINT-0001 shows the full BOM + routing story separately. Together they demonstrate that the framework expresses the customer's production workflow through configuration, not code. Smoke verified on fresh Postgres: all 7 items seeded, WO with 3 BOM + 3 ops created, SO confirm spawns 2 WOs with source traceability, SPA /sales-orders/new renders and creates orders.
-
First runnable end-to-end demo: open the browser, log in, click through every PBC, and walk a sales order DRAFT → CONFIRMED → SHIPPED. Stock balances drop, the SALES_SHIPMENT row appears in the ledger, and the AR journal entry settles — all visible in the SPA without touching curl. Bumps version to 0.29.0-SNAPSHOT. What landed ----------- * New `:web` Gradle subproject — Vite + React 18 + TypeScript + Tailwind 3.4. The Gradle wrapper is two `Exec` tasks (`npmInstall`, `npmBuild`) with proper inputs/outputs declared for incremental builds. Deliberately no node-gradle plugin — one less moving piece. * SPA architecture: hand-written typed REST client over `fetch` (auth header injection + 401 handler), AuthContext that decodes the JWT for display, ProtectedRoute, AppLayout with sidebar grouped by PBC, 16 page components covering the full v1 surface (Items, UoMs, Partners, Locations, Stock Balances + Movements, Sales Orders + detail w/ confirm/ship/cancel, Purchase Orders + detail w/ confirm/receive/cancel, Work Orders + detail w/ start/complete, Shop-Floor dashboard with 5s polling, Journal Entries). 211 KB JS / 21 KB CSS gzipped. * Sales-order detail page: confirm/ship/cancel verbs each refresh the order, the (SO-filtered) movements list, and the (SO-filtered) journal entries — so an operator watches the ledger row appear and the AR row settle in real time after a single click. Same pattern on the purchase-order detail page for the AP/RECEIPT side. * Shop-floor dashboard polls /api/v1/production/work-orders/shop- floor every 5s and renders one card per IN_PROGRESS WO with current operation, planned vs actual minutes (progress bar), and operations-completed. * `:distribution` consumes the SPA dist via a normal Gradle outgoing/incoming configuration: `:web` exposes `webStaticBundle`, `:distribution`'s `bundleWebStatic` Sync task copies it into `${buildDir}/web-static/static/`, and that parent directory is added to the main resources source set so Spring Boot serves the SPA from `classpath:/static/` out of the same fat-jar. Single artifact, no nginx, no CORS. * New `SpaController` in platform-bootstrap forwards every known SPA route prefix to `/index.html` so React Router's HTML5 history mode works on hard refresh / deep-link entry. Explicit list (12 prefixes) rather than catch-all so typoed API URLs still get an honest 404 instead of the SPA shell. * SecurityConfiguration restructured: keeps the public allowlist for /api/v1/auth + /api/v1/_meta + /v3/api-docs + /swagger-ui, then `/api/**` is `.authenticated()`, then SPA static assets + every SPA route prefix are `.permitAll()`. The order is load-bearing — putting `.authenticated()` for /api/** BEFORE the SPA permitAll preserves the framework's "API is always authenticated" invariant even with the SPA bundled in the same fat-jar. The SPA bundle itself is just HTML+CSS+JS so permitting it is correct; secrets are gated by /api/**. * New `DemoSeedRunner` in `:distribution` (gated behind `vibeerp.demo.seed=true`, set in application-dev.yaml only). Idempotent — the runner short-circuits if its sentinel item (DEMO-PAPER-A4) already exists. Seeds 5 items, 2 warehouses, 4 partners, opening stock for every item, one open DEMO-SO-0001 (50× business cards + 20× brochures, $720), one open DEMO-PO-0001 (10000× paper, $400). Every row carries the DEMO- prefix so it's trivially distinguishable from hand-created data; a future "delete demo data" command has an obvious filter. Production deploys never set the property, so the @ConditionalOnProperty bean stays absent from the context. How to run ---------- docker compose up -d db ./gradlew :distribution:bootRun open http://localhost:8080 # Read the bootstrap admin password from the boot log, # log in as admin, and walk DEMO-SO-0001 through the # confirm + ship flow to see the buy-sell loop in the UI. What was caught by the smoke test --------------------------------- * TypeScript strict mode + `error: unknown` in React state → `{error && <X/>}` evaluates to `unknown` and JSX rejects it. Fixed by typing the state as `Error | null` and converting in catches with `e instanceof Error ? e : new Error(String(e))`. Affected 16 page files; the conversion is now uniform. * DataTable's `T extends Record<string, unknown>` constraint was too restrictive for typed row interfaces; relaxed to unconstrained `T` with `(row as unknown as Record<…>)[key]` for the unkeyed cell read fallback. * `vite.config.ts` needs `@types/node` for `node:path` + `__dirname`; added to devDependencies and tsconfig.node.json declares `"types": ["node"]`. * KDoc nested-comment trap (4th time): SpaController's KDoc had `/api/v1/...` in backticks; the `/*` inside backticks starts a nested block comment and breaks Kotlin compilation with "Unclosed comment". Rephrased to "the api-v1 prefix". * SecurityConfiguration order: a draft version that put the SPA permit-all rules BEFORE `/api/**` authenticated() let unauthenticated requests reach API endpoints. Caught by an explicit smoke test (curl /api/v1/some-bogus-endpoint should return 401, not 404 from a missing static file). Reordered so /api/** authentication runs first. End-to-end smoke (real Postgres, fresh DB) ------------------------------------------ - bootRun starts in 7.6s - DemoSeedRunner reports "populating starter dataset… done" - GET / returns the SPA HTML - GET /sales-orders, /sales-orders/<uuid>, /journal-entries all return 200 (SPA shell — React Router takes over) - GET /assets/index-*.js / /assets/index-*.css both 200 - GET /api/v1/some-bogus-endpoint → 401 (Spring Security rejects before any controller mapping) - admin login via /api/v1/auth/login → 200 + JWT - GET /catalog/items → 5 DEMO-* rows - GET /partners/partners → 4 DEMO-* rows - GET /inventory/locations → 2 DEMO-* warehouses - GET /inventory/balances → 5 starting balances - POST /orders/sales-orders/<id>/confirm → CONFIRMED; GET /finance/journal-entries shows AR POSTED 720 USD - POST /orders/sales-orders/<id>/ship {"shippingLocationCode": "DEMO-WH-FG"} → SHIPPED; balances drop to 150 + 80; journal entry flips to SETTLED - POST /orders/purchase-orders/<id>/confirm → CONFIRMED; AP POSTED 400 USD appears - POST /orders/purchase-orders/<id>/receive → RECEIVED; PAPER balance grows from 5000 to 15000; AP row SETTLED - 8 stock_movement rows in the ledger total