-
Add 15 new message keys for the four Edit*Page components that were missed in the first pass. All hardcoded labels, hints, and error messages now go through useT().
-
Replace every hardcoded English string across all 35 SPA page files with useT() calls backed by message keys in messages.ts. Both en-US and zh-CN translations are provided for every key (~200 new keys). Pages updated: DashboardPage, LoginPage, FormDesignerPage, ListViewDesignerPage, MetadataAdminPage, SalesOrderDetailPage, PurchaseOrderDetailPage, WorkOrderDetailPage, ShopFloorPage, UserTasksPage, TaskDetailPage, UserDetailPage, all Create* pages, all Edit* pages, and all list pages (Items, Partners, Locations, Balances, Movements, SalesOrders, PurchaseOrders, WorkOrders, Users, Roles, Accounts, JournalEntries, UoMs, AdjustStock).
-
Backend: - Add update() method + UpdateWorkOrderCommand to WorkOrderService (DRAFT-only, mutable fields: outputQuantity, dueDate, ext) - Add PATCH /{id} endpoint + UpdateWorkOrderRequest to WorkOrderController - Add production.work-order.update permission to metadata YAML - Location, SalesOrder, PurchaseOrder already had PATCH endpoints Frontend: - Add getLocation, updateLocation to inventory client - Add update methods to salesOrders, purchaseOrders, production client - Create EditLocationPage (name, active, ext) - Create EditSalesOrderPage (ext only; DRAFT-only guard) - Create EditPurchaseOrderPage (ext only; DRAFT-only guard) - Create EditWorkOrderPage (outputQuantity, dueDate, ext; DRAFT-only guard) - Wire all four edit routes in App.tsx - Fix duplicate i18n keys in messages.ts (label.actions, label.fieldKey) -
When type is 'enum', show a comma-separated input for allowed values. When type is 'string', show a max-length input. Previously these type-specific options were missing from the UI.
-
407 tests, all green. Rules engine with 24 unit tests for condition evaluation. User-task form rendering via MetadataFormRenderer.
-
P3.5: event-condition-action rules stored as metadata rows, evaluated when events fire. v1.0 actions: log, set-field, publish-event. P2.3: SPA pages for listing and completing Flowable user-tasks using MetadataFormRenderer. P2.2 BPMN designer deferred to v1.1.
-
- P3.2: MetadataFormRenderer + @rjsf/core + 6 custom ERP widgets - P3.3: Form designer (structured property editor + live preview) - P3.6: List view designer (column/filter/sort configuration) - R3: Metadata admin (tabbed CRUD for all metadata types) - 382 tests, all green
-
Add the @rjsf-based metadata-driven form renderer with a custom Tailwind theme matching the existing SPA styling and six ERP-specific widgets: PartnerPicker, ItemPicker, UomSelector, LocationPicker, MoneyInput, and QuantityInput.
-
Add three write controllers behind @RequirePermission("admin.metadata.write"): - FormDefinitionController (PUT/DELETE /api/v1/_meta/metadata/forms/{slug}) - ListViewDefinitionController (PUT/DELETE /api/v1/_meta/metadata/list-views/{slug}) - CustomFieldWriteController (POST/PUT/DELETE /api/v1/_meta/metadata/custom-fields) All three enforce source='user' immutability: rows seeded by core or plug-in YAMLs cannot be modified or deleted through the REST surface. CustomFieldWriteController calls CustomFieldRegistry.refresh() after every successful write so the in-memory index stays current. MetadataController gains GET endpoints for forms, list views (including by-slug lookup), and the aggregate /metadata response now includes forms and listViews sections. Ships platform-metadata.yml declaring admin.metadata.read/write permissions and a Metadata Admin menu entry. 22 unit tests cover the full CRUD + source-enforcement matrix. -
9 tasks: backend YAML/loader extension, CRUD endpoints with source enforcement, @rjsf form renderer + custom widgets, form designer, list view designer, metadata admin tabbed UI, smoke test + version bump.
-
Sub-project A of remaining v1.0 work. Key decisions: - Hybrid: core forms stay handcrafted, renderer for user-task forms - @rjsf/core with custom ERP widget registry - Structured property editor (not drag-and-drop) for form designer - Custom entities deferred to v1.1
-
CLAUDE.md "Repository state" was stale (18→25 subprojects, 246→356 tests, 8→10 PBCs, 9→12 platform services). PROGRESS.md "What's not yet live" still listed Flowable, JasperReports, file store, job scheduler, OIDC, and web SPA as missing — all are live. "Current stage" paragraph updated to reflect 10 PBCs and 12 services. "How to run" section updated to remove DemoSeedRunner reference (moved to demo branch).
-
Tier 1 customization comes alive in the SPA: custom fields declared in YAML metadata now render automatically in create forms without any compile-time knowledge of the field. New component: DynamicExtFields - Fetches custom field declarations from the existing /api/v1/_meta/metadata/custom-fields/{entityName} endpoint - Renders one input per declared field, type-matched: string → text, integer → number (step=1), decimal/money/ quantity → number (step=0.01), boolean → checkbox, date → date picker, dateTime → datetime-local, enum → select dropdown, uuid → text - Labels resolve from labelTranslations using the active locale (i18n integration) - Required fields show a red asterisk - Values are collected in the ext map and sent with the create request Wired into: CreateItemPage (entityName="Item"), CreatePartnerPage (entityName="Partner"). Both now show a "Custom fields" section below the static fields when the entity has custom field declarations in metadata. No new backend code — the existing /api/v1/_meta/metadata/ custom-fields endpoint already returns exactly the shape the component needs. This is P3.1: the runtime form renderer for Tier 1 customization. -
Adds edit pages for items and partners — the two entities operators update most often. Each form loads the existing record, pre-fills all editable fields, and PATCHes on save. Code and baseUomCode are read-only after creation (by design). New pages: - EditItemPage: name, type, description, active toggle - EditPartnerPage: name, type, email, phone API client: catalog.updateItem, partners.update (PATCH). List pages: item/partner codes are now clickable links to the edit page instead of plain text. Routes wired at /items/:id/edit and /partners/:id/edit.
-
Adds client-side i18n infrastructure to the SPA (CLAUDE.md guardrail #6: global/i18n from day one). New files: - i18n/messages.ts: flat key-value message bundles for en-US and zh-CN. Keys use dot-notation (nav.*, action.*, status.*, label.*). ~80 keys per locale covering navigation, actions, status badges, and common labels. - i18n/LocaleContext.tsx: LocaleProvider + useT() hook + useLocale() hook. Active locale stored in localStorage, defaults to the browser's navigator.language. Auto-detects zh-* → zh-CN. Wired into the SPA: - main.tsx wraps the app in <LocaleProvider> - AppLayout sidebar uses t(key) for every heading and item - Top bar has a locale dropdown (English / 中文) that switches the entire sidebar + status labels instantly - StatusBadge uses t('status.DRAFT') etc. so statuses render as '草稿' / '已确认' / '已发货' in Chinese The i18n system is intentionally simple: plain strings, no ICU MessageFormat patterns (those live on the backend via ICU4J). A future chunk can adopt @formatjs/intl-messageformat if the SPA needs plural/gender/number formatting client-side. Not yet translated: page-level titles and form labels (they still use hard-coded English). The infrastructure is in place; translating individual pages is incremental. -
Removes demo-specific code from the main (framework) branch so main stays a clean, generic foundation. The demo branch retains these files from before this commit. Removed from main: - DemoSeedRunner.kt (printing-company seed data) - vibeerp.demo.seed config in application-dev.yaml - EBC-PP-001 demo walkthrough in DashboardPage The dashboard now shows a generic "Getting started" guide that walks operators through setting up master data, creating orders, and walking the buy-make-sell loop — without referencing any specific customer or seed data. To run the printing-company demo, use the demo worktree: cd ~/Desktop/vibe_erp_demo ./gradlew :distribution:bootRun
-
JournalEntriesPage now renders expandable debit/credit lines for each entry. Click a row to toggle the line detail view showing account code, DR/CR amounts, and description. Types updated: JournalEntry now includes lines array with JournalEntryLine (lineNo, accountCode, debit, credit, description). This makes the GL growth visible in the demo — confirming an SO shows the AR entry with DR 1100 / CR 4100 balanced lines inline.
-
Adds JournalEntryLine child entity with debit/credit legs per account, completing the pbc-finance GL foundation. Domain: - JournalEntryLine entity: lineNo, accountCode, debit (>=0), credit (>=0), description. FK to parent JournalEntry with CASCADE delete. Unique (journal_entry_id, line_no). - JournalEntry gains @OneToMany lines collection (EAGER fetch, ordered by lineNo). Event subscribers now write balanced double-entry lines: - SalesOrderConfirmed: DR 1100 (AR), CR 4100 (Revenue) - PurchaseOrderConfirmed: DR 1200 (Inventory), CR 2100 (AP) The parent JournalEntry.amount is retained as a denormalized summary; the lines are the source of truth for accounting. The seeded account codes (1100, 1200, 2100, 4100, 5100) match the chart from the previous commit. JournalEntryController.toResponse() now includes the lines array so the SPA can display debit/credit legs inline. Schema: 004-finance-entry-lines.xml adds finance__journal_entry_line with FK, unique (entry, lineNo), non-negative check on dr/cr. Smoke verified on fresh Postgres: - Confirm SO-2026-0001 -> AR entry with 2 lines: DR 1100 $1950, CR 4100 $1950 - Confirm PO-2026-0001 -> AP entry with 2 lines: DR 1200 $2550, CR 2100 $2550 - Both entries balanced (sum DR = sum CR) Caught by smoke: LazyInitializationException on the new lines collection — fixed by switching FetchType from LAZY to EAGER (entries have 2-4 lines max, eager is appropriate). -
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. -
Pins today's 5 feature commits: - 25353240 SPA CRUD forms (item, partner, PO, WO) - 82c5267d R2 identity screens (users, roles, assignment) - c2fab13b S3 file backend (P1.9 complete) - 6ad72c7c OIDC federation (P4.2 complete) - 17771894 SPA fill (create location, adjust stock) P1.9 promoted from Partial to DONE. P4.2 promoted from Pending to DONE. R2 promoted from Pending to DONE. R4 updated to reflect create forms for every manageable entity. Version bumped 0.29.0 -> 0.30.0-SNAPSHOT.
-
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). -
The framework now supports federated authentication: operators can configure an external OIDC provider (Keycloak, Auth0, or any OIDC-compliant issuer) and the API accepts JWTs from both the built-in auth (/api/v1/auth/login, HS256) and the OIDC provider (RS256, JWKS auto-discovered). Opt-in via vibeerp.security.oidc.issuer-uri. When blank (default), only built-in auth works — exactly the pre-P4.2 behavior. When set, the JwtDecoder becomes a composite: tries the built-in HS256 decoder first (cheap, local HMAC), falls back to the OIDC decoder (RS256, cached JWKS fetch from the provider's .well-known endpoint). Claim mapping: PrincipalContextFilter now handles both formats: - Built-in: sub=UUID, username=<claim>, roles=<flat array> - OIDC/Keycloak: sub=OIDC subject, preferred_username=<claim>, realm_access.roles=<nested array> Claim names are configurable via vibeerp.security.oidc.username-claim and roles-claim for non-Keycloak providers. New files: - OidcProperties.kt: config properties class for the OIDC block Modified files: - JwtConfiguration.kt: composite decoder, now takes OidcProperties - PrincipalContextFilter.kt: dual claim resolution (built-in first, OIDC fallback), now takes OidcProperties - JwtRoundTripTest.kt: updated to pass OidcProperties (defaults) - application.yaml: OIDC config block with env-var interpolation No new dependencies — uses Spring Security's existing JwtDecoders.fromIssuerLocation() which is already on the classpath via spring-boot-starter-oauth2-resource-server. -
Adds S3FileStorage alongside the existing LocalDiskFileStorage, selected at boot by vibeerp.files.backend (local or s3). The local backend is the default (matchIfMissing=true) so existing deployments are unaffected. Setting backend=s3 activates the S3 backend with its own config block. Works with AWS S3, MinIO, DigitalOcean Spaces, or any S3-compatible object store via the endpoint-url override. The S3 client is lazy-initialized on first use so the bean loads even when S3 is unreachable at boot time (useful for tests and for the local-disk default path where the S3 bean is never instantiated). Configuration (vibeerp.files.s3.*): - bucket (required when backend=s3) - region (default: us-east-1) - endpoint-url (optional; for MinIO and non-AWS services) - access-key + secret-key (optional; falls back to AWS DefaultCredentialsProvider chain) - key-prefix (optional; namespaces objects so multiple instances can share one bucket) Implementation notes: - put() reads the stream into a byte array for S3 (S3 requires Content-Length up front; chunked upload is a future optimization for large files) - get() returns the S3 response InputStream directly; caller must close it (same contract as local backend) - list() paginates via ContinuationToken for buckets with >1000 objects per prefix - Content-type is stored as native S3 object metadata (no sidecar .meta file unlike local backend) Dependency: software.amazon.awssdk:s3:2.28.6 (AWS SDK v2) added to libs.versions.toml and platform-files build.gradle.kts. LocalDiskFileStorage gained @ConditionalOnProperty(havingValue = "local", matchIfMissing = true) so it's the default but doesn't conflict when backend=s3. application.yaml updated with commented-out S3 config block documenting all available properties. -
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.
-
Updates the "at a glance" row to v0.29.0-SNAPSHOT + fc62d6d7, bumps the Phase 6 R1 row to DONE with the commit ref and an overview of what landed (Gradle wrapper, SpaController, security reordering, bundled fat-jar, 16 pages), and rewrites the "How to run" section to walk the click-through demo instead of just curl. README's status table updated to reflect 10/10 PBCs + 356 tests + SPA status; building section now mentions that `./gradlew build` compiles the SPA too. No code changes.