@cosmicdrift/kumiko-samples 0.399.0 → 0.400.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/package.json +1 -1
- package/packages/bundled-features/package.json +6 -1
- package/packages/bundled-features/src/documents/aggregate-id.ts +20 -0
- package/packages/bundled-features/src/documents/capture.ts +125 -0
- package/packages/bundled-features/src/documents/constants.ts +31 -0
- package/packages/bundled-features/src/documents/ensure-link.ts +35 -0
- package/packages/bundled-features/src/documents/entity.ts +103 -0
- package/packages/bundled-features/src/documents/executor.ts +13 -0
- package/packages/bundled-features/src/documents/expiry.ts +32 -0
- package/packages/bundled-features/src/documents/feature.ts +197 -0
- package/packages/bundled-features/src/documents/handlers/import.write.ts +157 -0
- package/packages/bundled-features/src/documents/handlers/link.write.ts +103 -0
- package/packages/bundled-features/src/documents/i18n.ts +35 -0
- package/packages/bundled-features/src/documents/index.ts +22 -0
- package/packages/bundled-features/src/documents/pending-file-guard.ts +36 -0
- package/packages/bundled-features/src/documents/queries/expiry-overview.query.ts +46 -0
- package/packages/bundled-features/src/documents/queries/for-record.query.ts +74 -0
- package/packages/bundled-features/src/documents/queries/list-page.ts +51 -0
- package/packages/bundled-features/src/documents/queries/overview.query.ts +45 -0
- package/packages/bundled-features/src/documents/schemas.ts +66 -0
- package/packages/bundled-features/src/documents/screens.ts +86 -0
- package/packages/bundled-features/src/documents/tab.ts +46 -0
- package/packages/bundled-features/src/documents/target-write-check.ts +41 -0
- package/packages/bundled-features/src/documents/write-guard.ts +59 -0
- package/packages/bundled-features/src/shared/purge-row-file-refs.ts +5 -4
- package/samples/apps/styleguide/src/app/seed.ts +102 -1
- package/samples/apps/styleguide/src/app/server.ts +2 -2
- package/samples/apps/styleguide/src/features/examples/feature.ts +97 -0
- package/samples/apps/styleguide/src/features/examples/i18n.ts +47 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +30 -1
- package/samples/apps/use-all-bundled/kumiko/migrations/0058_documents.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +11 -2
- package/samples/apps/use-all-bundled/src/run-config.ts +7 -0
- package/samples/recipes/derived-fields-sql/README.md +27 -0
- package/samples/recipes/derived-fields-sql/package.json +11 -0
- package/samples/recipes/derived-fields-sql/src/feature.ts +98 -0
- package/samples/recipes/documents/package.json +12 -0
- package/samples/recipes/documents/src/feature.ts +66 -0
- package/samples/recipes/inspection-checklist/README.md +27 -0
- package/samples/recipes/inspection-checklist/package.json +12 -0
- package/samples/recipes/inspection-checklist/src/entities/inspection-plan.ts +24 -0
- package/samples/recipes/inspection-checklist/src/entities/inspection-protocol.ts +72 -0
- package/samples/recipes/inspection-checklist/src/feature.ts +33 -0
- package/samples/recipes/inspection-checklist/src/handlers/create-from-plan.write.ts +38 -0
- package/samples/recipes/inspection-checklist/src/screens/inspection-protocol-edit.screen.ts +27 -0
|
@@ -43,6 +43,7 @@ import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features
|
|
|
43
43
|
import { createDeliveryFeature } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
44
44
|
import { createDerivativesSharpFeature } from "@cosmicdrift/kumiko-bundled-features/derivatives-sharp";
|
|
45
45
|
import { createDocumentIngestFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/document-ingest-foundation";
|
|
46
|
+
import { createDocumentsFeature } from "@cosmicdrift/kumiko-bundled-features/documents";
|
|
46
47
|
import {
|
|
47
48
|
createFeatureTogglesFeature,
|
|
48
49
|
FEATURE_TOGGLES_FEATURE,
|
|
@@ -406,6 +407,12 @@ export const APP_FEATURES = [
|
|
|
406
407
|
// user-data-rights — both mounted here.
|
|
407
408
|
createFormDraftUserDataFeature(),
|
|
408
409
|
createFoldersFeature(),
|
|
410
|
+
// documents: filed files visible through the records they link to; folder stands in for a host.
|
|
411
|
+
createDocumentsFeature({
|
|
412
|
+
categories: ["contract", "other"],
|
|
413
|
+
linkTargets: ["folder"],
|
|
414
|
+
adminRoles: ["TenantAdmin", "SystemAdmin"],
|
|
415
|
+
}),
|
|
409
416
|
// folders-user-data: GDPR hooks for folder entities. Depends (optionally)
|
|
410
417
|
// on folders + (hard) on user-data-rights — both mounted above.
|
|
411
418
|
createFoldersUserDataFeature(),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Derived fields (SQL)
|
|
2
|
+
|
|
3
|
+
A derived field is read-time only: no column, never in a write schema. A `sql` derived field is
|
|
4
|
+
computed inside the list SELECT as the viewer, so it can be sorted on the server and never
|
|
5
|
+
enters the entity cache. A viewer who cannot read a field the value depends on gets NULL.
|
|
6
|
+
|
|
7
|
+
## What it shows
|
|
8
|
+
|
|
9
|
+
- `coalesce`: the nickname when set, else the name.
|
|
10
|
+
- `related` + `count` with a `where`: open tickets per building.
|
|
11
|
+
- `related` + `first`: the title of the newest ticket.
|
|
12
|
+
- `related` + `first` on a money field with `valueType: "money"`: amount and currency come
|
|
13
|
+
back in the shape of a stored money field (no key when the value is unset).
|
|
14
|
+
- `sortable: true` on a derived field and an `entityList` `defaultSort` on it.
|
|
15
|
+
|
|
16
|
+
## Source
|
|
17
|
+
|
|
18
|
+
Feature entry point: `src/feature.ts`.
|
|
19
|
+
|
|
20
|
+
## Tests
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd samples/recipes/derived-fields-sql
|
|
24
|
+
bun test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`src/__tests__/feature.integration.test.ts` runs the list query over HTTP.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosmicdrift/kumiko-sample-derived-fields-sql",
|
|
3
|
+
"description": "SQL derived fields on a list: coalesce, related count with a where, related first (text and money), sortable columns and a defaultSort on a derived value.",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@cosmicdrift/kumiko-framework": "workspace:*"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@cosmicdrift/kumiko-testing": "workspace:*"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDateField,
|
|
3
|
+
createDerivedField,
|
|
4
|
+
createEntity,
|
|
5
|
+
createMoneyField,
|
|
6
|
+
createSelectField,
|
|
7
|
+
createTextField,
|
|
8
|
+
defineFeature,
|
|
9
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
|
|
11
|
+
const openAccess = {
|
|
12
|
+
access: {
|
|
13
|
+
openToAll: { reason: "recipe: any signed-in user reads and edits; no per-user ownership" },
|
|
14
|
+
},
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const ticketEntity = createEntity({
|
|
18
|
+
table: "read_derived_sql_tickets",
|
|
19
|
+
fields: {
|
|
20
|
+
buildingId: { type: "reference", entity: "building" },
|
|
21
|
+
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
22
|
+
status: createSelectField({ options: ["open", "inProgress", "done"] as const }),
|
|
23
|
+
openedOn: createDateField({ required: true }),
|
|
24
|
+
cost: createMoneyField(),
|
|
25
|
+
},
|
|
26
|
+
// The sortable derived columns of the building run a subquery per row over this column.
|
|
27
|
+
indexes: [{ columns: ["buildingId"] }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const buildingEntity = createEntity({
|
|
31
|
+
table: "read_derived_sql_buildings",
|
|
32
|
+
fields: {
|
|
33
|
+
name: createTextField({
|
|
34
|
+
personal: false,
|
|
35
|
+
reason: "is_business_data",
|
|
36
|
+
required: true,
|
|
37
|
+
searchable: true,
|
|
38
|
+
}),
|
|
39
|
+
nickname: createTextField({ personal: false, reason: "is_business_data" }),
|
|
40
|
+
},
|
|
41
|
+
// Computed inside the list SELECT as the viewer; never stored, never cached.
|
|
42
|
+
derivedFields: {
|
|
43
|
+
// First non-null of the columns, so the list shows the nickname when there is one.
|
|
44
|
+
shownName: createDerivedField({
|
|
45
|
+
valueType: "text",
|
|
46
|
+
sortable: true,
|
|
47
|
+
sql: { kind: "coalesce", fields: ["nickname", "name"] },
|
|
48
|
+
}),
|
|
49
|
+
openTickets: createDerivedField({
|
|
50
|
+
valueType: "number",
|
|
51
|
+
sortable: true,
|
|
52
|
+
sql: {
|
|
53
|
+
kind: "related",
|
|
54
|
+
entity: "ticket",
|
|
55
|
+
on: { field: "buildingId" },
|
|
56
|
+
measure: { kind: "count" },
|
|
57
|
+
where: [{ field: "status", op: "in", value: ["open", "inProgress"] }],
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
latestTicket: createDerivedField({
|
|
61
|
+
valueType: "text",
|
|
62
|
+
sql: {
|
|
63
|
+
kind: "related",
|
|
64
|
+
entity: "ticket",
|
|
65
|
+
on: { field: "buildingId" },
|
|
66
|
+
measure: { kind: "first", field: "title", orderBy: "openedOn", direction: "desc" },
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
// Money reads amount and currency together and has the shape of a stored money field.
|
|
70
|
+
latestCost: createDerivedField({
|
|
71
|
+
valueType: "money",
|
|
72
|
+
sortable: true,
|
|
73
|
+
sql: {
|
|
74
|
+
kind: "related",
|
|
75
|
+
entity: "ticket",
|
|
76
|
+
on: { field: "buildingId" },
|
|
77
|
+
measure: { kind: "first", field: "cost", orderBy: "openedOn", direction: "desc" },
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const buildingListSort = { field: "openTickets", dir: "desc" } as const;
|
|
84
|
+
|
|
85
|
+
export const derivedFieldsSqlFeature = defineFeature("derived-fields-sql", (r) => {
|
|
86
|
+
r.crud("ticket", ticketEntity, { write: openAccess, read: openAccess });
|
|
87
|
+
r.crud("building", buildingEntity, { write: openAccess, read: openAccess });
|
|
88
|
+
|
|
89
|
+
r.screen({
|
|
90
|
+
id: "building-list",
|
|
91
|
+
type: "entityList",
|
|
92
|
+
entity: "building",
|
|
93
|
+
columns: ["shownName", "openTickets", "latestTicket", "latestCost"],
|
|
94
|
+
// A derived column sorts on the server only when it is `sortable: true`.
|
|
95
|
+
defaultSort: buildingListSort,
|
|
96
|
+
access: openAccess.access,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosmicdrift/kumiko-sample-documents",
|
|
3
|
+
"description": "A document register on top of existing records: captured files, expiry dates and per-record visibility.",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@cosmicdrift/kumiko-bundled-features": "workspace:*",
|
|
7
|
+
"@cosmicdrift/kumiko-framework": "workspace:*"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@cosmicdrift/kumiko-testing": "workspace:*"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// kumiko-feature-version: 1
|
|
2
|
+
// Documents Sample
|
|
3
|
+
//
|
|
4
|
+
// A `contract` entity with a file field becomes a document source without knowing about the
|
|
5
|
+
// documents feature: the capture rule below files every saved contract file as a document of
|
|
6
|
+
// category "contract" and links it to the contract and to its property. Who sees the document
|
|
7
|
+
// follows from who may read the linked records and the contract that owns the file.
|
|
8
|
+
|
|
9
|
+
import { createDocumentsFeature } from "@cosmicdrift/kumiko-bundled-features/documents";
|
|
10
|
+
import {
|
|
11
|
+
createEntity,
|
|
12
|
+
createFileField,
|
|
13
|
+
createTextField,
|
|
14
|
+
defineEntityCreateHandler,
|
|
15
|
+
defineEntityDetailHandler,
|
|
16
|
+
defineEntityListHandler,
|
|
17
|
+
defineEntityUpdateHandler,
|
|
18
|
+
defineFeature,
|
|
19
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
|
|
21
|
+
const text = () =>
|
|
22
|
+
createTextField({ personal: false, reason: "is_business_data", required: true, maxLength: 120 });
|
|
23
|
+
|
|
24
|
+
export const propertyEntity = createEntity({
|
|
25
|
+
table: "read_sample_documents_properties",
|
|
26
|
+
fields: { name: text() },
|
|
27
|
+
access: { read: { PropertyReader: "all", TenantAdmin: "all" } },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const contractEntity = createEntity({
|
|
31
|
+
table: "read_sample_documents_contracts",
|
|
32
|
+
fields: { name: text(), propertyId: text(), file: createFileField() },
|
|
33
|
+
access: { read: { ContractReader: "all", TenantAdmin: "all" } },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const access = { roles: ["PropertyReader", "ContractReader", "TenantAdmin"] };
|
|
37
|
+
|
|
38
|
+
export const propertyManagement = defineFeature("property-management", (r) => {
|
|
39
|
+
r.requires({ features: ["files", "documents"] });
|
|
40
|
+
r.entity({ name: "property", ...propertyEntity });
|
|
41
|
+
r.entity({ name: "contract", ...contractEntity });
|
|
42
|
+
for (const [name, entity] of [
|
|
43
|
+
["property", propertyEntity],
|
|
44
|
+
["contract", contractEntity],
|
|
45
|
+
] as const) {
|
|
46
|
+
r.writeHandler(defineEntityCreateHandler(name, entity, { access }));
|
|
47
|
+
r.writeHandler(defineEntityUpdateHandler(name, entity, { access: { roles: ["TenantAdmin"] } }));
|
|
48
|
+
r.queryHandler(defineEntityListHandler(name, entity, { access }));
|
|
49
|
+
r.queryHandler(defineEntityDetailHandler(name, entity, { access }));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const documents = createDocumentsFeature({
|
|
54
|
+
categories: ["contract", "insurance"],
|
|
55
|
+
linkTargets: ["property", "contract"],
|
|
56
|
+
access,
|
|
57
|
+
capture: [
|
|
58
|
+
{
|
|
59
|
+
entity: "contract",
|
|
60
|
+
field: "file",
|
|
61
|
+
category: "contract",
|
|
62
|
+
title: (row) => `Contract ${String(row["name"])}`,
|
|
63
|
+
links: (row) => [{ entityType: "property", entityId: String(row["propertyId"]) }],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Inspection checklist
|
|
2
|
+
|
|
3
|
+
An embedded list as a phone inspection checklist: the inspector walks fixed rows,
|
|
4
|
+
enters a measurement or a result, and documents deviations with a finding and photos.
|
|
5
|
+
|
|
6
|
+
## What it shows
|
|
7
|
+
|
|
8
|
+
- **Conditional cells** (`src/entities/inspection-protocol.ts`): `visibleWhen` and
|
|
9
|
+
`requiredWhen` on sub-fields and on top-level fields. The server enforces
|
|
10
|
+
`requiredWhen` on create and update and answers with one `required` path per
|
|
11
|
+
missing cell (`checkpoints.0.finding`, `condition`).
|
|
12
|
+
- **Pass/fail by range**: the derived `inRange` cell turns `measured`, `min` and `max`
|
|
13
|
+
into `ok` or `not_ok` (bounds included). The server recomputes it, so a client-sent
|
|
14
|
+
result never survives a value outside the range. `when` limits it to measurement rows;
|
|
15
|
+
a check row keeps the result the inspector picked.
|
|
16
|
+
- **Photos per row**: an `images` sub-field. Upload with `entityType`, `fieldName` of the
|
|
17
|
+
embedded list and `subField` of the photo cell; files are bound, tenant-checked and
|
|
18
|
+
disposed like a top-level images field, always under the embedded list's field name.
|
|
19
|
+
- **Fixed rows**: `inspection-protocol:create-from-plan` copies label, kind and limits from
|
|
20
|
+
a plan; the edit screen keeps those cells read-only and the row count fixed. It sets
|
|
21
|
+
`layout.width: "full"` because the default form width pushes result, finding and photos
|
|
22
|
+
behind horizontal scroll.
|
|
23
|
+
|
|
24
|
+
## Source
|
|
25
|
+
|
|
26
|
+
`src/feature.ts` wires the two entities, the handler and the screen. The integration test
|
|
27
|
+
covers every behavior above over real HTTP.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosmicdrift/kumiko-sample-inspection-checklist",
|
|
3
|
+
"description": "Embedded list as a phone inspection checklist: conditional fields, derived pass/fail, photos per row.",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@cosmicdrift/kumiko-bundled-features": "workspace:*",
|
|
7
|
+
"@cosmicdrift/kumiko-framework": "workspace:*"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@cosmicdrift/kumiko-testing": "workspace:*"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
createEmbeddedListField,
|
|
4
|
+
createEntity,
|
|
5
|
+
createTextField,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
|
|
8
|
+
export const inspectionPlanEntity = createEntity({
|
|
9
|
+
table: "read_sample_inspection_plans",
|
|
10
|
+
fields: {
|
|
11
|
+
name: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
12
|
+
checkpoints: createEmbeddedListField(
|
|
13
|
+
{
|
|
14
|
+
label: { type: "text", required: true },
|
|
15
|
+
kind: { type: "select", options: ["check", "measurement"], required: true },
|
|
16
|
+
min: { type: "number" },
|
|
17
|
+
max: { type: "number" },
|
|
18
|
+
},
|
|
19
|
+
{ personal: false, reason: "is_business_data" },
|
|
20
|
+
),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const inspectionPlanTable = buildEntityTable("inspection-plan", inspectionPlanEntity);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
createEmbeddedListField,
|
|
4
|
+
createEntity,
|
|
5
|
+
createSelectField,
|
|
6
|
+
createTextField,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
|
|
9
|
+
const isMeasurement = { field: "kind", eq: "measurement" } as const;
|
|
10
|
+
const isNotOk = { field: "result", eq: "not_ok" } as const;
|
|
11
|
+
|
|
12
|
+
export const inspectionProtocolEntity = createEntity({
|
|
13
|
+
table: "read_sample_inspection_protocols",
|
|
14
|
+
fields: {
|
|
15
|
+
measuringDevice: createTextField({ personal: false, reason: "is_business_data" }),
|
|
16
|
+
checkpoints: createEmbeddedListField(
|
|
17
|
+
{
|
|
18
|
+
label: { type: "text", required: true },
|
|
19
|
+
kind: { type: "select", options: ["check", "measurement"], required: true },
|
|
20
|
+
min: { type: "number", visibleWhen: isMeasurement },
|
|
21
|
+
max: { type: "number", visibleWhen: isMeasurement },
|
|
22
|
+
measured: { type: "number", visibleWhen: isMeasurement },
|
|
23
|
+
result: {
|
|
24
|
+
type: "select",
|
|
25
|
+
options: ["ok", "not_ok"],
|
|
26
|
+
display: "radio",
|
|
27
|
+
optionTones: { ok: "ok", not_ok: "bad" },
|
|
28
|
+
},
|
|
29
|
+
finding: { type: "text", visibleWhen: isNotOk, requiredWhen: isNotOk },
|
|
30
|
+
photos: {
|
|
31
|
+
type: "images",
|
|
32
|
+
capture: "environment",
|
|
33
|
+
maxCount: 5,
|
|
34
|
+
accept: ["jpg", "jpeg", "png"],
|
|
35
|
+
visibleWhen: isNotOk,
|
|
36
|
+
requiredWhen: isNotOk,
|
|
37
|
+
},
|
|
38
|
+
documents: { type: "files", maxCount: 3 },
|
|
39
|
+
approvedBy: { type: "text", access: { write: ["Admin"] } },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
personal: false,
|
|
43
|
+
reason: "is_business_data",
|
|
44
|
+
derived: {
|
|
45
|
+
result: {
|
|
46
|
+
op: "inRange",
|
|
47
|
+
from: ["measured", "min", "max"],
|
|
48
|
+
inside: "ok",
|
|
49
|
+
outside: "not_ok",
|
|
50
|
+
when: isMeasurement,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
continuedOperation: createSelectField({
|
|
56
|
+
options: ["yes", "with_condition", "no"],
|
|
57
|
+
display: "radio",
|
|
58
|
+
optionTones: { yes: "ok", with_condition: "warn", no: "bad" },
|
|
59
|
+
}),
|
|
60
|
+
condition: createTextField({
|
|
61
|
+
personal: false,
|
|
62
|
+
reason: "is_business_data",
|
|
63
|
+
visibleWhen: { field: "continuedOperation", eq: "with_condition" },
|
|
64
|
+
requiredWhen: { field: "continuedOperation", eq: "with_condition" },
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const inspectionProtocolTable = buildEntityTable(
|
|
70
|
+
"inspection-protocol",
|
|
71
|
+
inspectionProtocolEntity,
|
|
72
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Inspection Checklist Sample
|
|
2
|
+
// Shows: an embedded list as a phone inspection checklist.
|
|
3
|
+
// - visibleWhen / requiredWhen on cells and top-level fields (server-enforced)
|
|
4
|
+
// - a derived pass/fail cell (inRange) that the server recomputes
|
|
5
|
+
// - an images cell per row, uploaded with the `subField` multipart part
|
|
6
|
+
// - fixed rows copied from a plan, only the result cells stay editable
|
|
7
|
+
|
|
8
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import { inspectionPlanEntity } from "./entities/inspection-plan";
|
|
10
|
+
import { inspectionProtocolEntity } from "./entities/inspection-protocol";
|
|
11
|
+
import { createFromPlan } from "./handlers/create-from-plan.write";
|
|
12
|
+
import { inspectionProtocolEditScreen } from "./screens/inspection-protocol-edit.screen";
|
|
13
|
+
|
|
14
|
+
export { inspectionPlanEntity } from "./entities/inspection-plan";
|
|
15
|
+
export { inspectionProtocolEntity } from "./entities/inspection-protocol";
|
|
16
|
+
|
|
17
|
+
const inspectorAccess = { access: { roles: ["Admin", "Inspector"] } } as const;
|
|
18
|
+
|
|
19
|
+
export const inspectionChecklistFeature = defineFeature("inspection-checklist", (r) => {
|
|
20
|
+
r.requires({ features: ["files"] });
|
|
21
|
+
|
|
22
|
+
r.crud("inspection-plan", inspectionPlanEntity, {
|
|
23
|
+
write: inspectorAccess,
|
|
24
|
+
read: inspectorAccess,
|
|
25
|
+
});
|
|
26
|
+
r.crud("inspection-protocol", inspectionProtocolEntity, {
|
|
27
|
+
write: inspectorAccess,
|
|
28
|
+
read: inspectorAccess,
|
|
29
|
+
});
|
|
30
|
+
r.writeHandler(createFromPlan);
|
|
31
|
+
|
|
32
|
+
r.screen(inspectionProtocolEditScreen);
|
|
33
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createEntityExecutor, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { inspectionPlanEntity } from "../entities/inspection-plan";
|
|
5
|
+
import { inspectionProtocolEntity } from "../entities/inspection-protocol";
|
|
6
|
+
|
|
7
|
+
const { executor: planExecutor } = createEntityExecutor("inspection-plan", inspectionPlanEntity);
|
|
8
|
+
const { executor: protocolExecutor } = createEntityExecutor(
|
|
9
|
+
"inspection-protocol",
|
|
10
|
+
inspectionProtocolEntity,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const planRows = z.array(
|
|
14
|
+
z.object({
|
|
15
|
+
label: z.string(),
|
|
16
|
+
kind: z.enum(["check", "measurement"]),
|
|
17
|
+
min: z.number().optional(),
|
|
18
|
+
max: z.number().optional(),
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Copies only the plan's fixed columns; result, finding and photos stay empty for the inspector.
|
|
23
|
+
export const createFromPlan = defineWriteHandler({
|
|
24
|
+
name: "inspection-protocol:create-from-plan",
|
|
25
|
+
schema: z.object({ planId: z.uuid() }),
|
|
26
|
+
access: { roles: ["Admin", "Inspector"] },
|
|
27
|
+
handler: async (event, ctx) => {
|
|
28
|
+
const plan = await planExecutor.detail({ id: event.payload.planId }, event.user, ctx.db);
|
|
29
|
+
if (!plan) return failNotFound("inspection-plan", event.payload.planId);
|
|
30
|
+
const checkpoints = planRows.parse(plan["checkpoints"]).map(({ label, kind, min, max }) => ({
|
|
31
|
+
label,
|
|
32
|
+
kind,
|
|
33
|
+
...(min !== undefined ? { min } : {}),
|
|
34
|
+
...(max !== undefined ? { max } : {}),
|
|
35
|
+
}));
|
|
36
|
+
return protocolExecutor.create({ checkpoints }, event.user, ctx.db);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
|
|
3
|
+
export const inspectionProtocolEditScreen: EntityEditScreenDefinition = {
|
|
4
|
+
id: "inspection-protocol-edit",
|
|
5
|
+
type: "entityEdit",
|
|
6
|
+
entity: "inspection-protocol",
|
|
7
|
+
layout: {
|
|
8
|
+
width: "full",
|
|
9
|
+
sections: [
|
|
10
|
+
{
|
|
11
|
+
columns: 1,
|
|
12
|
+
fields: [
|
|
13
|
+
"measuringDevice",
|
|
14
|
+
{
|
|
15
|
+
field: "checkpoints",
|
|
16
|
+
embeddedList: {
|
|
17
|
+
readOnlyCells: ["label", "kind", "min", "max"],
|
|
18
|
+
cardHiddenCells: ["kind"],
|
|
19
|
+
fixedRows: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{ columns: 1, fields: ["continuedOperation", "condition"] },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
};
|