@sparkerp/plugin-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/bundle/blocks.json +12229 -0
- package/bundle/catalog.json +32475 -0
- package/bundle/docs/applications/hcm/employee-profile.md +144 -0
- package/bundle/docs/concepts/artifact-types.md +80 -0
- package/bundle/docs/concepts/glossary.md +124 -0
- package/bundle/docs/concepts/overview.md +88 -0
- package/bundle/docs/concepts/page-data-flow.md +84 -0
- package/bundle/docs/concepts/plugin-lifecycle.md +81 -0
- package/bundle/docs/concepts/sdk-modes.md +71 -0
- package/bundle/docs/docs.json +109 -0
- package/bundle/docs/guides/add-a-cadence-job.md +134 -0
- package/bundle/docs/guides/add-a-compliance-job.md +139 -0
- package/bundle/docs/guides/add-a-create-edit-form.md +167 -0
- package/bundle/docs/guides/add-a-custom-block.md +225 -0
- package/bundle/docs/guides/add-a-data-provider.md +216 -0
- package/bundle/docs/guides/add-a-kpi.md +154 -0
- package/bundle/docs/guides/add-a-reminder-job.md +181 -0
- package/bundle/docs/guides/add-an-aggregation-job.md +153 -0
- package/bundle/docs/guides/add-an-approval-workflow.md +222 -0
- package/bundle/docs/guides/add-an-entity.md +170 -0
- package/bundle/docs/guides/add-business-rules.md +147 -0
- package/bundle/docs/guides/add-i18n.md +121 -0
- package/bundle/docs/guides/add-menus.md +149 -0
- package/bundle/docs/guides/build-a-code-plugin.md +141 -0
- package/bundle/docs/guides/build-a-page.md +213 -0
- package/bundle/docs/guides/build-a-polyglot-service-plugin.md +201 -0
- package/bundle/docs/guides/build-a-tenant-extension-service.md +149 -0
- package/bundle/docs/guides/create-a-plugin.md +172 -0
- package/bundle/docs/guides/expose-a-plugin-operation-as-an-ai-tool.md +114 -0
- package/bundle/docs/guides/index.md +53 -0
- package/bundle/docs/guides/publish-and-upgrade.md +152 -0
- package/bundle/docs/guides/responsive-plugin.md +117 -0
- package/bundle/docs/guides/seed-data-on-install.md +127 -0
- package/bundle/docs/guides/set-up-the-sdk.md +120 -0
- package/bundle/docs/guides/use-the-mcp-server.md +104 -0
- package/bundle/docs/guides/validate-and-test.md +119 -0
- package/bundle/docs/guides/wire-a-pages-data.md +153 -0
- package/bundle/docs/quickstart/index.md +118 -0
- package/bundle/docs/recipes/condition-based-compliance-flag.md +70 -0
- package/bundle/docs/recipes/cross-plugin-offboarding-fanout.md +68 -0
- package/bundle/docs/recipes/cross-row-aggregation.md +67 -0
- package/bundle/docs/recipes/entity-with-approval-workflow.md +72 -0
- package/bundle/docs/recipes/index.md +40 -0
- package/bundle/docs/recipes/install-lifecycle-data-seeding.md +76 -0
- package/bundle/docs/recipes/kpi-dashboard-page.md +92 -0
- package/bundle/docs/recipes/master-detail-page.md +84 -0
- package/bundle/docs/recipes/n-days-before-reminder.md +72 -0
- package/bundle/docs/recipes/per-row-document-generator.md +65 -0
- package/bundle/docs/recipes/related-record-lookup-field.md +74 -0
- package/bundle/docs/recipes/resolved-reference-grid-column.md +63 -0
- package/bundle/docs/recipes/scheduled-record-generation.md +68 -0
- package/bundle/docs/reference/action.md +18 -0
- package/bundle/docs/reference/block-definition.md +35 -0
- package/bundle/docs/reference/block-instance.md +33 -0
- package/bundle/docs/reference/composite-pack-manifest.md +33 -0
- package/bundle/docs/reference/connector-definition.md +36 -0
- package/bundle/docs/reference/data-service-definition.md +22 -0
- package/bundle/docs/reference/data-view-definition.md +26 -0
- package/bundle/docs/reference/entity-aggregation-config.md +38 -0
- package/bundle/docs/reference/entity-cadence-config.md +33 -0
- package/bundle/docs/reference/entity-compliance-config.md +37 -0
- package/bundle/docs/reference/entity-cross-plugin-action-config.md +37 -0
- package/bundle/docs/reference/entity-definition.md +18 -0
- package/bundle/docs/reference/entity-document-generator-config.md +35 -0
- package/bundle/docs/reference/entity-rule-definition.md +29 -0
- package/bundle/docs/reference/entity-status-date-sweep-config.md +27 -0
- package/bundle/docs/reference/form.md +35 -0
- package/bundle/docs/reference/index.md +37 -0
- package/bundle/docs/reference/menu.md +26 -0
- package/bundle/docs/reference/mobile-nav.md +22 -0
- package/bundle/docs/reference/page-template.md +29 -0
- package/bundle/docs/reference/page.md +44 -0
- package/bundle/docs/reference/plugin-manifest.md +54 -0
- package/bundle/docs/reference/plugin-seed-data.md +21 -0
- package/bundle/docs/reference/print-template-definition.md +22 -0
- package/bundle/docs/reference/provider-definition.md +22 -0
- package/bundle/docs/reference/report-definition.md +22 -0
- package/bundle/docs/reference/rule.md +30 -0
- package/bundle/docs/reference/workflow-definition.md +28 -0
- package/bundle/docs/troubleshooting/grid-shows-zero-rows.md +82 -0
- package/bundle/docs/troubleshooting/job-never-runs.md +89 -0
- package/bundle/docs/troubleshooting/known-gotchas.md +193 -0
- package/bundle/docs/troubleshooting/publishing-errors.md +119 -0
- package/bundle/docs/tutorial/01-create-the-plugin.md +157 -0
- package/bundle/docs/tutorial/02-category-entity-and-seed.md +94 -0
- package/bundle/docs/tutorial/03-equipment-entity.md +121 -0
- package/bundle/docs/tutorial/04-equipment-register-page.md +153 -0
- package/bundle/docs/tutorial/05-checkout-entity-and-page.md +116 -0
- package/bundle/docs/tutorial/06-checkout-approval-workflow.md +139 -0
- package/bundle/docs/tutorial/07-return-due-reminder-job.md +129 -0
- package/bundle/docs/tutorial/08-menus-i18n-publish.md +123 -0
- package/bundle/docs/tutorial/index.md +72 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-aggregation-configs.json +18 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cadence-configs.json +9 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-compliance-configs.json +74 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cross-plugin-configs.json +9 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-document-configs.json +9 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-sweep-configs.json +260 -0
- package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/entities/hcm_salary_planning_entry.json +41 -0
- package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/page/salary-planning.json +1 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/page/promotions.json +10944 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.movement.json +44 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.promotion-acknowledge.json +19 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.resignation.json +29 -0
- package/bundle/examples/example-patterns.mjs +94 -0
- package/bundle/manifest.json +14 -0
- package/bundle/schemas/action.schema.json +102 -0
- package/bundle/schemas/block-definition.schema.json +222 -0
- package/bundle/schemas/block-instance.schema.json +139 -0
- package/bundle/schemas/composite-pack-manifest.schema.json +160 -0
- package/bundle/schemas/connector-definition.schema.json +29 -0
- package/bundle/schemas/data-service-definition.schema.json +64 -0
- package/bundle/schemas/data-view-definition.schema.json +148 -0
- package/bundle/schemas/entity-aggregation-config.schema.json +32 -0
- package/bundle/schemas/entity-cadence-config.schema.json +27 -0
- package/bundle/schemas/entity-compliance-config.schema.json +31 -0
- package/bundle/schemas/entity-cross-plugin-action-config.schema.json +31 -0
- package/bundle/schemas/entity-definition.schema.json +126 -0
- package/bundle/schemas/entity-document-generator-config.schema.json +29 -0
- package/bundle/schemas/entity-rule-definition.schema.json +75 -0
- package/bundle/schemas/entity-status-date-sweep-config.schema.json +21 -0
- package/bundle/schemas/form.schema.json +281 -0
- package/bundle/schemas/menu.schema.json +106 -0
- package/bundle/schemas/mobile-nav.schema.json +53 -0
- package/bundle/schemas/page-template.schema.json +46 -0
- package/bundle/schemas/page.schema.json +292 -0
- package/bundle/schemas/plugin-manifest.schema.json +139 -0
- package/bundle/schemas/plugin-seed-data.schema.json +24 -0
- package/bundle/schemas/print-template-definition.schema.json +90 -0
- package/bundle/schemas/provider-definition.schema.json +27 -0
- package/bundle/schemas/report-definition.schema.json +96 -0
- package/bundle/schemas/rule.schema.json +82 -0
- package/bundle/schemas/workflow-definition.schema.json +42 -0
- package/bundle/validators/block-engine.mjs +23222 -0
- package/bundle/validators/page-engine.mjs +21645 -0
- package/erp-cli/authoring-root.mjs +344 -0
- package/erp-cli/erp.mjs +4065 -0
- package/erp-cli/example-patterns.mjs +94 -0
- package/erp-mcp-server/index.mjs +818 -0
- package/package.json +27 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "1. Create the plugin"
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 1. Create the plugin
|
|
7
|
+
|
|
8
|
+
## Scaffold
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
erp plugin create example-plugin --name "Office Equipment" --type business-app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> The folder is `example-plugin/` to match this docs tree. The plugin **id** we
|
|
15
|
+
> use is `office-equipment` — set below. (In real life pick one name and use it
|
|
16
|
+
> for both.)
|
|
17
|
+
|
|
18
|
+
You get `example-plugin/spk-assembly/` with empty `metadata/*` folders and a
|
|
19
|
+
`plugin.json` with `"mainClass": null`.
|
|
20
|
+
|
|
21
|
+
## Edit the manifest
|
|
22
|
+
|
|
23
|
+
`spk-assembly/plugin.json` — the finished version
|
|
24
|
+
([real file](./example-plugin/spk-assembly/plugin.json)):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"id": "office-equipment",
|
|
29
|
+
"name": "Office Equipment",
|
|
30
|
+
"version": "1.0.3",
|
|
31
|
+
"type": "business-application",
|
|
32
|
+
"schemaName": "erp_core",
|
|
33
|
+
"vendor": "ACME Corp",
|
|
34
|
+
"license": "Proprietary",
|
|
35
|
+
"licenseTier": "free",
|
|
36
|
+
"category": "custom",
|
|
37
|
+
"erpVersion": "*",
|
|
38
|
+
"minErpVersion": "1.0",
|
|
39
|
+
"maxErpVersion": "99.0",
|
|
40
|
+
"mainClass": null,
|
|
41
|
+
"dependencies": [],
|
|
42
|
+
"optionalDependencies": [],
|
|
43
|
+
"owner": "business",
|
|
44
|
+
"editable": false,
|
|
45
|
+
"extendable": true,
|
|
46
|
+
"capabilitiesProvided": [],
|
|
47
|
+
"capabilitiesRequired": [],
|
|
48
|
+
"featureFlags": [],
|
|
49
|
+
"configSchemaJson": null,
|
|
50
|
+
"roles": [
|
|
51
|
+
{
|
|
52
|
+
"name": "Office Equipment Manager",
|
|
53
|
+
"code": "OFFICE_EQUIPMENT_MANAGER",
|
|
54
|
+
"description": "Full control of the Office Equipment module.",
|
|
55
|
+
"permissionKeys": [
|
|
56
|
+
"OeqCategory.View", "OeqCategory.Create", "OeqCategory.Edit",
|
|
57
|
+
"OeqEquipment.View", "OeqEquipment.Create", "OeqEquipment.Edit",
|
|
58
|
+
"OeqCheckout.View", "OeqCheckout.Create", "OeqCheckout.Submit", "OeqCheckout.Approve",
|
|
59
|
+
"Dashboard.View"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"runtimeModes": ["embedded"],
|
|
64
|
+
"serviceDeployment": null
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Key choices:
|
|
69
|
+
|
|
70
|
+
- **`id: "office-equipment"`** — used in every route, page id, and i18n key
|
|
71
|
+
from here on. Chosen once.
|
|
72
|
+
- **`role.name` and `role.code`** are prefixed (`Office Equipment Manager` /
|
|
73
|
+
`OFFICE_EQUIPMENT_MANAGER`). A plain "Equipment Manager" would collide with
|
|
74
|
+
another installed plugin's role and fail the install — this actually happened
|
|
75
|
+
while writing the tutorial.
|
|
76
|
+
- **`schemaName: "erp_core"`** — the scaffold default. See the note in
|
|
77
|
+
[Add an entity](../guides/add-an-entity.md) about when a dedicated schema is
|
|
78
|
+
worth it; for this module `erp_core` is fine end to end, including the
|
|
79
|
+
reminder job in chapter 7.
|
|
80
|
+
|
|
81
|
+
## No Java class
|
|
82
|
+
|
|
83
|
+
This tutorial module ships **zero Java**. `plugin.json` keeps `"mainClass":
|
|
84
|
+
null` exactly as the scaffold wrote it, and there is no `spk-assembly/lib/`.
|
|
85
|
+
On install the platform loads the plugin through a built-in generic no-op PF4J
|
|
86
|
+
entry point — start/stop/enable/disable/upgrade, migrations, and data seeding
|
|
87
|
+
all run normally. Everything the module does is JSON under
|
|
88
|
+
`spk-assembly/metadata/`.
|
|
89
|
+
|
|
90
|
+
You would only add a Java class (a `Plugin` subclass, compiled against `pf4j` +
|
|
91
|
+
`slf4j-api`, jar in `spk-assembly/lib/`, `mainClass` set to its FQN) for
|
|
92
|
+
behaviour the Entity / Query / Rule / Workflow / Job engines genuinely can't
|
|
93
|
+
express — this module never needs one.
|
|
94
|
+
|
|
95
|
+
## The application and module (standalone plugin)
|
|
96
|
+
|
|
97
|
+
A standalone plugin (one that isn't part of a bigger suite like HCM) ships its
|
|
98
|
+
own application and module so its pages are routable.
|
|
99
|
+
|
|
100
|
+
`spk-assembly/metadata/application/office-equipment-app.json`
|
|
101
|
+
([real file](./example-plugin/spk-assembly/metadata/application/office-equipment-app.json)):
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"name": "office-equipment-app",
|
|
106
|
+
"description": "The Office Equipment plugin's own Application.",
|
|
107
|
+
"definition": {
|
|
108
|
+
"id": "office-equipment",
|
|
109
|
+
"titleKey": "office-equipment.application.title",
|
|
110
|
+
"urlSlug": "office-equipment",
|
|
111
|
+
"homeRoute": "/app/office-equipment/equipment/office-equipment/equipment-register",
|
|
112
|
+
"tier": "solution",
|
|
113
|
+
"icon": "devices",
|
|
114
|
+
"color": "#2563EB"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`spk-assembly/metadata/module/office-equipment-module.json`
|
|
120
|
+
([real file](./example-plugin/spk-assembly/metadata/module/office-equipment-module.json)):
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"name": "office-equipment",
|
|
125
|
+
"description": "The single Module the Office Equipment pages attach to.",
|
|
126
|
+
"definition": {
|
|
127
|
+
"id": "office-equipment-module",
|
|
128
|
+
"titleKey": "office-equipment.module.title",
|
|
129
|
+
"urlSlug": "equipment",
|
|
130
|
+
"tier": "solution",
|
|
131
|
+
"icon": "inventory_2",
|
|
132
|
+
"color": "#2563EB"
|
|
133
|
+
},
|
|
134
|
+
"applications": ["office-equipment-app"]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The reachable URL for a page is
|
|
139
|
+
`/app/<app urlSlug>/<module urlSlug>/<pluginId>/<page name>` — so
|
|
140
|
+
`/app/office-equipment/equipment/office-equipment/equipment-register`.
|
|
141
|
+
|
|
142
|
+
## Verify
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
erp plugin test example-plugin/spk-assembly
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Plugin Tests
|
|
150
|
+
────────────────────────────────────────
|
|
151
|
+
✓ plugin.json:valid-json
|
|
152
|
+
1 passed, 0 failed, 0 semantic warning(s), 3 skipped
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A healthy empty plugin. Next: give it a table.
|
|
156
|
+
|
|
157
|
+
**Next:** [2. The equipment category entity + seeded data](./02-category-entity-and-seed.md)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "2. The equipment category entity + seeded data"
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 2. The equipment category entity + seeded data
|
|
7
|
+
|
|
8
|
+
## The entity
|
|
9
|
+
|
|
10
|
+
`spk-assembly/metadata/entities/oeq_category.json`
|
|
11
|
+
([real file](./example-plugin/spk-assembly/metadata/entities/oeq_category.json)):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"entity": {
|
|
16
|
+
"name": "oeq_category",
|
|
17
|
+
"tableName": "oeq_category",
|
|
18
|
+
"label": "{\"en\":\"Equipment Category\"}",
|
|
19
|
+
"category": "office-equipment",
|
|
20
|
+
"icon": "category",
|
|
21
|
+
"color": "#2563EB",
|
|
22
|
+
"pkStrategy": "identity"
|
|
23
|
+
},
|
|
24
|
+
"fields": [
|
|
25
|
+
{ "fieldName": "category_code", "label": "\"Category Code\"", "dataType": "text", "nullable": false, "required": true, "isUnique": true, "isIndexed": true, "isDisplayField": true, "displayOrder": 1 },
|
|
26
|
+
{ "fieldName": "category_name", "label": "\"Category Name\"", "dataType": "text", "nullable": false, "required": true, "displayOrder": 2 },
|
|
27
|
+
{ "fieldName": "description", "label": "\"Description\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 3 },
|
|
28
|
+
{ "fieldName": "status", "label": "\"Status\"", "dataType": "enum", "nullable": false, "required": true, "isIndexed": true, "defaultValue": "ACTIVE", "typeParams": "{\"enumValues\":[\"ACTIVE\",\"INACTIVE\"]}", "displayOrder": 4 },
|
|
29
|
+
{ "fieldName": "seeded_by", "label": "\"Seeded By\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 5 }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Note the two JSON-encoding rules: `entity.label` is `"{\"en\":\"...\"}"` (an
|
|
35
|
+
i18n object as a string), each field `label` is `"\"...\""` (a JSON string
|
|
36
|
+
literal). A bare string in either place fails the install.
|
|
37
|
+
|
|
38
|
+
`seeded_by` is a plain text column the seeder will stamp with the plugin id.
|
|
39
|
+
|
|
40
|
+
## The seed data
|
|
41
|
+
|
|
42
|
+
`spk-assembly/metadata/seed-data/office-equipment-categories.json`
|
|
43
|
+
([real file](./example-plugin/spk-assembly/metadata/seed-data/office-equipment-categories.json)):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"entity": "oeq_category",
|
|
48
|
+
"keyFields": ["category_code"],
|
|
49
|
+
"source": "office-equipment",
|
|
50
|
+
"rows": [
|
|
51
|
+
{ "category_code": "LAPTOP", "category_name": "Laptops", "description": "Portable computers", "status": "ACTIVE", "seeded_by": "office-equipment" },
|
|
52
|
+
{ "category_code": "MONITOR", "category_name": "Monitors", "description": "External displays", "status": "ACTIVE", "seeded_by": "office-equipment" },
|
|
53
|
+
{ "category_code": "PHONE", "category_name": "Phones", "description": "Mobile handsets", "status": "ACTIVE", "seeded_by": "office-equipment" },
|
|
54
|
+
{ "category_code": "HEADSET", "category_name": "Headsets", "description": "Audio headsets", "status": "ACTIVE", "seeded_by": "office-equipment" }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `keyFields: ["category_code"]` — the natural key. Re-installs match on this and
|
|
60
|
+
never duplicate.
|
|
61
|
+
- `source: "office-equipment"` — stamped into `seeded_by`.
|
|
62
|
+
|
|
63
|
+
## Verify
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
erp schema validate spk-assembly/metadata/entities/oeq_category.json --schema entity-definition
|
|
67
|
+
erp schema validate spk-assembly/metadata/seed-data/office-equipment-categories.json --schema plugin-seed-data
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Both print `OK — ... matches schema ...`.
|
|
71
|
+
|
|
72
|
+
After the module is published (chapter 8), this is what the seeding did — from
|
|
73
|
+
the real install log:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
PluginDataSeedInstaller: office-equipment-categories.json -> entity oeq_category (tenant 2): 4 created, 0 updated, 0 unchanged
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
and the rows are live:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ erp api get "/api/v1/entities/oeq_category/records/query?size=10"
|
|
83
|
+
{ "rows": [
|
|
84
|
+
{ "id": 1, "category_code": "LAPTOP", "category_name": "Laptops", "status": "ACTIVE", "seeded_by": "office-equipment" },
|
|
85
|
+
{ "id": 2, "category_code": "MONITOR", "category_name": "Monitors", "status": "ACTIVE", "seeded_by": "office-equipment" },
|
|
86
|
+
{ "id": 3, "category_code": "PHONE", "category_name": "Phones", "status": "ACTIVE", "seeded_by": "office-equipment" },
|
|
87
|
+
{ "id": 4, "category_code": "HEADSET", "category_name": "Headsets", "status": "ACTIVE", "seeded_by": "office-equipment" }
|
|
88
|
+
], "total": 4 }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Publish again with a fifth category and only that one is inserted — the seeder is
|
|
92
|
+
idempotent by `keyFields`.
|
|
93
|
+
|
|
94
|
+
**Next:** [3. The equipment register entity](./03-equipment-entity.md)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "3. The equipment register entity"
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 3. The equipment register entity
|
|
7
|
+
|
|
8
|
+
## The entity
|
|
9
|
+
|
|
10
|
+
`spk-assembly/metadata/entities/oeq_equipment.json`
|
|
11
|
+
([real file](./example-plugin/spk-assembly/metadata/entities/oeq_equipment.json)):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"entity": {
|
|
16
|
+
"name": "oeq_equipment",
|
|
17
|
+
"tableName": "oeq_equipment",
|
|
18
|
+
"label": "{\"en\":\"Equipment\"}",
|
|
19
|
+
"category": "office-equipment",
|
|
20
|
+
"icon": "devices",
|
|
21
|
+
"color": "#2563EB",
|
|
22
|
+
"pkStrategy": "identity"
|
|
23
|
+
},
|
|
24
|
+
"fields": [
|
|
25
|
+
{ "fieldName": "asset_tag", "label": "\"Asset Tag\"", "dataType": "text", "nullable": false, "required": true, "isUnique": true, "isIndexed": true, "isDisplayField": true, "displayOrder": 1 },
|
|
26
|
+
{ "fieldName": "name", "label": "\"Name\"", "dataType": "text", "nullable": false, "required": true, "displayOrder": 2 },
|
|
27
|
+
{ "fieldName": "category_id", "label": "\"Category Id\"", "dataType": "long", "nullable": true, "required": false, "isIndexed": true, "displayOrder": 3 },
|
|
28
|
+
{ "fieldName": "category_name", "label": "\"Category\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 4 },
|
|
29
|
+
{ "fieldName": "serial_number", "label": "\"Serial Number\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 5 },
|
|
30
|
+
{ "fieldName": "status", "label": "\"Status\"", "dataType": "enum", "nullable": false, "required": true, "isIndexed": true, "defaultValue": "AVAILABLE", "typeParams": "{\"enumValues\":[\"AVAILABLE\",\"ASSIGNED\",\"MAINTENANCE\",\"RETIRED\"]}", "displayOrder": 6 },
|
|
31
|
+
{ "fieldName": "purchase_date", "label": "\"Purchase Date\"", "dataType": "date", "nullable": true, "required": false, "displayOrder": 7 },
|
|
32
|
+
{ "fieldName": "warranty_expiry_date", "label": "\"Warranty Expiry Date\"", "dataType": "date", "nullable": true, "required": false, "displayOrder": 8 },
|
|
33
|
+
{ "fieldName": "notes", "label": "\"Notes\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 9 }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Design notes
|
|
39
|
+
|
|
40
|
+
- **`category_id` (long) + `category_name` (text)** — we *denormalize* the
|
|
41
|
+
category name so the grid can show it without a join. The create dialog writes
|
|
42
|
+
both. (The alternative — a `reference` flag — needs a registered provider;
|
|
43
|
+
chapter 5 uses that for the employee field, where one exists.)
|
|
44
|
+
- **`status` enum** with `AVAILABLE` as default.
|
|
45
|
+
- **`asset_tag`** is `isDisplayField` and `isUnique`.
|
|
46
|
+
|
|
47
|
+
## The provider
|
|
48
|
+
|
|
49
|
+
Every entity you want a grid over needs a data provider.
|
|
50
|
+
`spk-assembly/metadata/provider/oeq-equipment-provider.json`
|
|
51
|
+
([real file](./example-plugin/spk-assembly/metadata/provider/oeq-equipment-provider.json)):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "oeq-equipment-provider",
|
|
56
|
+
"description": "Data Provider for oeq_equipment (grid + CRUD).",
|
|
57
|
+
"definition": {
|
|
58
|
+
"kind": "rest",
|
|
59
|
+
"connectionRef": "self",
|
|
60
|
+
"basePath": "/api/v1/entities/oeq_equipment/records",
|
|
61
|
+
"supports": ["search", "get", "create", "update"]
|
|
62
|
+
},
|
|
63
|
+
"metadata": {}, "modules": []
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The category lookup plumbing
|
|
68
|
+
|
|
69
|
+
The register page's create dialog will have a **category picker**. That needs a
|
|
70
|
+
Data View producing `{ value, label }` pairs and a `search` Data Service over it.
|
|
71
|
+
|
|
72
|
+
`spk-assembly/metadata/data_view/oeq-category-select-options-view.json`
|
|
73
|
+
([real file](./example-plugin/spk-assembly/metadata/data_view/oeq-category-select-options-view.json)) —
|
|
74
|
+
selects `t.id` as `value` and a concatenated `category_code - category_name` as
|
|
75
|
+
`label`.
|
|
76
|
+
|
|
77
|
+
`spk-assembly/metadata/data_service/oeq-category-search.json`
|
|
78
|
+
([real file](./example-plugin/spk-assembly/metadata/data_service/oeq-category-search.json)):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"name": "oeq-category-search",
|
|
83
|
+
"description": "Typeahead category search for the category lookup field.",
|
|
84
|
+
"definition": {
|
|
85
|
+
"operation": "search",
|
|
86
|
+
"source": { "kind": "dataView", "dataViewName": "oeq-category-select-options-view" },
|
|
87
|
+
"filters": [ { "field": "t.category_name", "operator": "contains", "value": "${param.search}" } ],
|
|
88
|
+
"parameters": [ { "name": "search", "type": "string" } ]
|
|
89
|
+
},
|
|
90
|
+
"metadata": {}, "modules": []
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## The KPI Data Services
|
|
95
|
+
|
|
96
|
+
The register page shows Total / Available / Assigned. Three `count` services plus
|
|
97
|
+
a `composite`:
|
|
98
|
+
|
|
99
|
+
- `oeq-equipment-count-total.json` — `count`, no filter
|
|
100
|
+
- `oeq-equipment-count-available.json` — `count`, `filters: [{ "field": "status", "operator": "eq", "value": "AVAILABLE" }]`
|
|
101
|
+
- `oeq-equipment-count-assigned.json` — `count`, filter `status = ASSIGNED`
|
|
102
|
+
- `oeq-equipment-kpis.json` — `composite` with three steps `total`/`available`/`assigned`
|
|
103
|
+
|
|
104
|
+
([all four real files](./example-plugin/spk-assembly/metadata/data_service/))
|
|
105
|
+
|
|
106
|
+
## Verify
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
node developer-docs/examples/test-examples.mjs
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
validates every one of these files against its schema. After publishing:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
$ erp api post "/api/v1/data-services/oeq-equipment-kpis/execute" --body "{}"
|
|
116
|
+
{ "results": { "total": { "value": 0 }, "available": { "value": 0 }, "assigned": { "value": 0 } } }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
That `results.<step>.value` shape is what the page will bind.
|
|
120
|
+
|
|
121
|
+
**Next:** [4. The equipment register page](./04-equipment-register-page.md)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "4. The equipment register page"
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 4. The equipment register page
|
|
7
|
+
|
|
8
|
+
The full page file is large — every widget is spelled out — so read
|
|
9
|
+
[`equipment-register.json`](./example-plugin/spk-assembly/metadata/page/equipment-register.json)
|
|
10
|
+
open in another tab. This chapter walks its structure section by section.
|
|
11
|
+
|
|
12
|
+
## The wrapper
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"name": "equipment-register",
|
|
17
|
+
"description": "Office Equipment tutorial - Equipment Register.",
|
|
18
|
+
"definition": {
|
|
19
|
+
"contractVersion": 1,
|
|
20
|
+
"id": "office-equipment.equipment-register",
|
|
21
|
+
"version": "1.0.0",
|
|
22
|
+
"publisher": "office-equipment",
|
|
23
|
+
"title": "office-equipment.equipment-register.title",
|
|
24
|
+
"usesAppShell": true,
|
|
25
|
+
"route": { "pattern": "/office-equipment/equipment-register", "params": [] },
|
|
26
|
+
"designer": {
|
|
27
|
+
"displayNameKey": "office-equipment.equipment-register.title",
|
|
28
|
+
"descriptionKey": "office-equipment.equipment-register.title",
|
|
29
|
+
"icon": "devices", "category": "list", "propertyGroups": [], "preview": { "kind": "page" }
|
|
30
|
+
},
|
|
31
|
+
"rows": [ { "id": "row-1", "columns": [ { "id": "col-1", "items": [ /* the 6 sections */ ] } ] } ]
|
|
32
|
+
},
|
|
33
|
+
"metadata": { "dataSource": "oeq-equipment-provider" },
|
|
34
|
+
"modules": ["office-equipment"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **`metadata.dataSource: "oeq-equipment-provider"`** — the whole grid wiring. It
|
|
39
|
+
matches the provider file's `name`. Page-level.
|
|
40
|
+
- **`modules: ["office-equipment"]`** — the module `name`, so the page lists in
|
|
41
|
+
the sidenav.
|
|
42
|
+
- **`id`** must be `<pluginId>.<name>`.
|
|
43
|
+
|
|
44
|
+
## Section 1 — the mount loader
|
|
45
|
+
|
|
46
|
+
A hidden `core.container` whose `mounted` event calls the KPI composite and
|
|
47
|
+
seeds page state:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
"events": { "mounted": { "source": "action-chain", "actions": [
|
|
51
|
+
{ "id": "a0", "order": 0, "type": "callApi",
|
|
52
|
+
"config": { "connectionRef": "self", "path": "/api/v1/data-services/oeq-equipment-kpis/execute", "httpMethod": "POST", "params": {} },
|
|
53
|
+
"output": "kpi" },
|
|
54
|
+
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.kpiTotal", "value": "${kpi.results.total.value}" } },
|
|
55
|
+
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.kpiAvailable", "value": "${kpi.results.available.value}" } },
|
|
56
|
+
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.kpiAssigned", "value": "${kpi.results.assigned.value}" } },
|
|
57
|
+
{ "id": "a4", "order": 4, "type": "setValue", "config": { "field": "page.gridRefreshTick", "value": 0 } },
|
|
58
|
+
{ "id": "a5", "order": 5, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": false } }
|
|
59
|
+
] } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`${kpi.results.total.value}` — composite response shape. This was verified live.
|
|
63
|
+
|
|
64
|
+
## Section 2 — the header
|
|
65
|
+
|
|
66
|
+
A flex `core.container` with the title/subtitle labels on the left and two
|
|
67
|
+
buttons on the right: **Export CSV** (a `downloadFile` action to
|
|
68
|
+
`/api/v1/entities/oeq_equipment/records/export?format=csv`) and **New** (clears
|
|
69
|
+
the form keys, opens the dialog).
|
|
70
|
+
|
|
71
|
+
## Section 3 — the KPI row
|
|
72
|
+
|
|
73
|
+
Three `core.kpi-card` blocks bound to `page.kpiTotal` / `kpiAvailable` /
|
|
74
|
+
`kpiAssigned`, each with a `labelKey`, `format: "number"`, `colorToken`, `icon`.
|
|
75
|
+
|
|
76
|
+
## Section 4 — the filter row
|
|
77
|
+
|
|
78
|
+
A `core.text-input` bound to `page.gridExternalFilter.search` and a
|
|
79
|
+
`core.select` bound to `page.gridExternalFilter.status`. Each has a `committed`
|
|
80
|
+
handler writing `${event.new}` back to its key.
|
|
81
|
+
|
|
82
|
+
## Section 5 — the grid
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"blockType": "core.grid",
|
|
87
|
+
"properties": {
|
|
88
|
+
"columns": { "source": "static", "value": [
|
|
89
|
+
{ "name": "asset_tag", "type": "string", "headerKey": "office-equipment.equipment-register.column.asset_tag" },
|
|
90
|
+
{ "name": "name", "type": "string", "headerKey": "...column.name" },
|
|
91
|
+
{ "name": "category_name", "type": "string", "headerKey": "...column.category_name" },
|
|
92
|
+
{ "name": "serial_number", "type": "string", "headerKey": "...column.serial_number" },
|
|
93
|
+
{ "name": "status", "type": "string", "headerKey": "...column.status", "renderAs": "chip",
|
|
94
|
+
"colorMap": { "AVAILABLE": "success", "ASSIGNED": "info", "MAINTENANCE": "warning", "RETIRED": "default" } }
|
|
95
|
+
] },
|
|
96
|
+
"pageSize": { "source": "static", "value": 25 },
|
|
97
|
+
"externalFilter": { "source": "binding", "binding": { "scope": "page", "key": "gridExternalFilter" } },
|
|
98
|
+
"refreshTrigger": { "source": "binding", "binding": { "scope": "page", "key": "gridRefreshTick" } }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The grid has **no** data property — it reads the page's `metadata.dataSource`.
|
|
104
|
+
`category_name` shows the denormalized value.
|
|
105
|
+
|
|
106
|
+
## Section 6 — the create dialog
|
|
107
|
+
|
|
108
|
+
A `core.dialog` bound to `page.formDialogOpen`. Inside: text inputs for asset
|
|
109
|
+
tag, name, serial number, notes; a `core.select` for status; `core.date-picker`
|
|
110
|
+
for the two dates; and a **`core.lookup`** for the category:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"blockType": "core.lookup",
|
|
115
|
+
"properties": {
|
|
116
|
+
"value": { "source": "binding", "binding": { "scope": "page", "key": "formCategoryId" } },
|
|
117
|
+
"recordType": { "source": "static", "value": "OeqCategory" },
|
|
118
|
+
"optionsSourceKey": { "source": "static", "value": "oeq-category-search" }
|
|
119
|
+
},
|
|
120
|
+
"events": { "committed": { "source": "action-chain", "actions": [
|
|
121
|
+
{ "id": "a0", "order": 0, "type": "setValue", "config": { "field": "page.formCategoryId", "value": "${event.new}" } }
|
|
122
|
+
] } }
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The Save button `POST`s to `/api/v1/entities/oeq_equipment/records` with a
|
|
127
|
+
`params` map built from `${page.form*}`, closes the dialog, toggles
|
|
128
|
+
`page.gridRefreshTick`, and shows a toast.
|
|
129
|
+
|
|
130
|
+
## Verify
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
erp plugin test example-plugin/spk-assembly
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
✓ page:equipment-register.json
|
|
138
|
+
4 passed, 0 failed, 0 semantic warning(s), 3 skipped
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`0 semantic warning(s)` means PAGE-DS-001 (data source wiring) and I18N-001
|
|
142
|
+
(missing keys) both passed. After publish, drive the Save chain from the API:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
$ erp api post "/api/v1/entities/oeq_equipment/records" \
|
|
146
|
+
--body '{"asset_tag":"LAP-001","name":"Dell Latitude 7440","category_id":1,"category_name":"Laptops","status":"AVAILABLE"}'
|
|
147
|
+
{ "id": 1, "asset_tag": "LAP-001", "name": "Dell Latitude 7440", "status": "AVAILABLE",
|
|
148
|
+
"created_by": "you@example.com", "created_at": "2026-09-10T08:08:33.282+00:00" }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
That row now appears in the grid.
|
|
152
|
+
|
|
153
|
+
**Next:** [5. The checkout entity, page, and lookups](./05-checkout-entity-and-page.md)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "5. The checkout entity, page, and lookups"
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 5. The checkout entity, page, and lookups
|
|
7
|
+
|
|
8
|
+
## The entity
|
|
9
|
+
|
|
10
|
+
`spk-assembly/metadata/entities/oeq_checkout.json`
|
|
11
|
+
([real file](./example-plugin/spk-assembly/metadata/entities/oeq_checkout.json)):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"entity": {
|
|
16
|
+
"name": "oeq_checkout",
|
|
17
|
+
"tableName": "oeq_checkout",
|
|
18
|
+
"label": "{\"en\":\"Equipment Checkout\"}",
|
|
19
|
+
"category": "office-equipment",
|
|
20
|
+
"icon": "assignment_ind",
|
|
21
|
+
"color": "#2563EB",
|
|
22
|
+
"pkStrategy": "identity"
|
|
23
|
+
},
|
|
24
|
+
"fields": [
|
|
25
|
+
{ "fieldName": "checkout_number", "label": "\"Checkout Number\"", "dataType": "text", "nullable": false, "required": true, "isUnique": true, "isIndexed": true, "isDisplayField": true, "displayOrder": 1 },
|
|
26
|
+
{ "fieldName": "equipment_id", "label": "\"Equipment Id\"", "dataType": "long", "nullable": false, "required": true, "isIndexed": true, "displayOrder": 2 },
|
|
27
|
+
{ "fieldName": "equipment_tag", "label": "\"Equipment\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 3 },
|
|
28
|
+
{ "fieldName": "employee_id", "label": "\"Employee Id\"", "dataType": "long", "nullable": false, "required": true, "isIndexed": true, "flags": "{\"reference\":{\"entityType\":\"Employee\"}}", "displayOrder": 4 },
|
|
29
|
+
{ "fieldName": "checkout_date", "label": "\"Checkout Date\"", "dataType": "date", "nullable": false, "required": true, "displayOrder": 5 },
|
|
30
|
+
{ "fieldName": "due_date", "label": "\"Due Date\"", "dataType": "date", "nullable": false, "required": true, "isIndexed": true, "displayOrder": 6 },
|
|
31
|
+
{ "fieldName": "purpose", "label": "\"Purpose\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 9 },
|
|
32
|
+
{ "fieldName": "status", "label": "\"Status\"", "dataType": "enum", "nullable": false, "required": true, "isIndexed": true, "defaultValue": "DRAFT", "typeParams": "{\"enumValues\":[\"DRAFT\",\"PENDING_APPROVAL\",\"APPROVED\",\"CHECKED_OUT\",\"DUE_SOON\",\"OVERDUE\",\"RETURNED\",\"REJECTED\"]}", "displayOrder": 10 }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### The reference flag
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
"flags": "{\"reference\":{\"entityType\":\"Employee\"}}"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
on `employee_id` turns on automatic id → name resolution. Every read of an
|
|
44
|
+
`oeq_checkout` row now includes a sibling `employee_id_label`. **Verified live:**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$ erp api post "/api/v1/entities/oeq_checkout/records" \
|
|
48
|
+
--body '{"checkout_number":"CO-001","equipment_id":3,"employee_id":1,"checkout_date":"2026-09-10","due_date":"2026-10-10","status":"CHECKED_OUT"}'
|
|
49
|
+
{ "id": 1, "checkout_number": "CO-001", "employee_id": 1,
|
|
50
|
+
"employee_id_label": "Aria Chen", "status": "CHECKED_OUT" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`"Employee"` is a registered provider (backed by the HCM employee plugin), so the
|
|
54
|
+
flag works. For `equipment` — which is *your own* entity, with no registered
|
|
55
|
+
provider — we denormalize `equipment_tag` instead.
|
|
56
|
+
|
|
57
|
+
### The status enum
|
|
58
|
+
|
|
59
|
+
`DUE_SOON` and `OVERDUE` are in the enum because the reminder job (chapter 7)
|
|
60
|
+
flips the status into them.
|
|
61
|
+
|
|
62
|
+
## The lookups
|
|
63
|
+
|
|
64
|
+
The checkout create dialog needs an **equipment** picker and an **employee**
|
|
65
|
+
picker.
|
|
66
|
+
|
|
67
|
+
- **Equipment** — your own entity, so build the pair:
|
|
68
|
+
`oeq-equipment-select-options-view.json` (Data View: `t.id` → `value`,
|
|
69
|
+
`asset_tag - name` → `label`) and `oeq-equipment-search.json` (`search` Data
|
|
70
|
+
Service over it). The dialog's `core.lookup` sets
|
|
71
|
+
`optionsSourceKey: "oeq-equipment-search"`.
|
|
72
|
+
- **Employee** — a registered reference type. The `core.lookup` sets
|
|
73
|
+
`optionsSourceKey: "employee-search"` (the platform-provided search service)
|
|
74
|
+
and `recordType: "Employee"`.
|
|
75
|
+
|
|
76
|
+
## The page
|
|
77
|
+
|
|
78
|
+
[`equipment-checkout.json`](./example-plugin/spk-assembly/metadata/page/equipment-checkout.json)
|
|
79
|
+
follows the same 6-section shape as the register page, with two differences:
|
|
80
|
+
|
|
81
|
+
1. **KPIs** are Currently Out / Overdue, from `oeq-checkout-kpis` (a `composite`
|
|
82
|
+
of `count` services filtered on `status = CHECKED_OUT` and `status = OVERDUE`).
|
|
83
|
+
2. **A detail dialog.** The grid's `rowClicked` fetches the record into
|
|
84
|
+
`page.detailRecord` and opens `page.detailDialogOpen`. Inside, an overview
|
|
85
|
+
card shows `detailRecord.checkout_number`, `equipment_tag`,
|
|
86
|
+
`employee_id_label`, `status`, and an actions card with a **Submit for
|
|
87
|
+
approval** button (chapter 6) and a Close button.
|
|
88
|
+
|
|
89
|
+
The grid's status column chip map includes the new values:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
"colorMap": { "DRAFT": "default", "PENDING_APPROVAL": "warning", "APPROVED": "info",
|
|
93
|
+
"CHECKED_OUT": "success", "DUE_SOON": "warning", "OVERDUE": "error",
|
|
94
|
+
"RETURNED": "default", "REJECTED": "error" }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Verify
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
erp plugin test example-plugin/spk-assembly
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
✓ page:equipment-checkout.json
|
|
105
|
+
✓ page:equipment-register.json
|
|
106
|
+
✓ page:equipment-catalog.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
After publish, the reference flag and the KPI service:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
$ erp api post "/api/v1/data-services/oeq-checkout-kpis/execute" --body "{}"
|
|
113
|
+
{ "results": { "open": { "value": 1 }, "overdue": { "value": 0 } } }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Next:** [6. The checkout approval workflow](./06-checkout-approval-workflow.md)
|