@adcops/autocore-react 3.3.84 → 3.3.87
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/dist/components/ValueInput.css +9 -12
- package/dist/components/ValueInput.d.ts +45 -154
- package/dist/components/ValueInput.d.ts.map +1 -1
- package/dist/components/ValueInput.js +1 -1
- package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
- package/dist/components/ams/AssetEditDialog.js +1 -1
- package/dist/components/forms/FormRow.d.ts +20 -0
- package/dist/components/forms/FormRow.d.ts.map +1 -0
- package/dist/components/forms/FormRow.js +1 -0
- package/dist/components/forms/FormSection.d.ts +19 -0
- package/dist/components/forms/FormSection.d.ts.map +1 -0
- package/dist/components/forms/FormSection.js +1 -0
- package/dist/components/forms/forms.css +89 -0
- package/dist/components/forms/index.d.ts +3 -0
- package/dist/components/forms/index.d.ts.map +1 -0
- package/dist/components/forms/index.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.css +121 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/TisConfigEditor.js +1 -0
- package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
- package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
- package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
- package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
- package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
- package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
- package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
- package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
- package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
- package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
- package/dist/components/tis-editor/types.d.ts +78 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -0
- package/dist/components/tis-editor/types.js +1 -0
- package/dist/components/tis-editor/validation.d.ts +20 -0
- package/dist/components/tis-editor/validation.d.ts.map +1 -0
- package/dist/components/tis-editor/validation.js +1 -0
- package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
- package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
- package/dist/hooks/useAmsAssetTypes.js +1 -0
- package/dist/hooks/useTisConfig.d.ts +51 -0
- package/dist/hooks/useTisConfig.d.ts.map +1 -0
- package/dist/hooks/useTisConfig.js +1 -0
- package/package.json +9 -3
- package/src/components/ValueInput.css +9 -12
- package/src/components/ValueInput.tsx +132 -317
- package/src/components/ams/AssetEditDialog.tsx +357 -20
- package/src/components/forms/FormRow.tsx +37 -0
- package/src/components/forms/FormSection.tsx +39 -0
- package/src/components/forms/forms.css +89 -0
- package/src/components/forms/index.ts +2 -0
- package/src/components/tis-editor/TisConfigEditor.css +121 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
- package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
- package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
- package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
- package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
- package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
- package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
- package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
- package/src/components/tis-editor/types.ts +95 -0
- package/src/components/tis-editor/validation.ts +104 -0
- package/src/hooks/useAmsAssetTypes.ts +70 -0
- package/src/hooks/useTisConfig.ts +164 -0
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* <AssetEditDialog> — modal for editing an existing asset
|
|
3
|
-
* nameplate (custom fields), and per-axis sub_locations matrix.
|
|
2
|
+
* <AssetEditDialog> — modal for editing an existing asset.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Two tabs:
|
|
5
|
+
* 1. "Asset" — role, nameplate (custom fields), and per-axis
|
|
6
|
+
* sub_locations matrix. Posts via `ams.update_asset`.
|
|
7
|
+
* 2. "Calibration" — performed_by, expires_at, cert_ref, notes, and
|
|
8
|
+
* the calibration values (flat or per-axis). Enabled only when
|
|
9
|
+
* `asset.current_calibration_id` is non-null. Posts via
|
|
10
|
+
* `ams.update_calibration` (server enforces "current only"). Tab
|
|
11
|
+
* is hidden entirely for assets that never had a calibration —
|
|
12
|
+
* "+ Calibration" on <AssetDetailView> is the path to add one.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* The server treats `asset_id`, `asset_type`, `serial`, and
|
|
15
|
+
* `install_date` as immutable; the read-only header strip mirrors
|
|
16
|
+
* that. Status stays out of this dialog because the Retire button on
|
|
17
|
+
* <AssetDetailView> already owns that transition.
|
|
18
|
+
*
|
|
19
|
+
* Save commits the Asset tab first, then (if a calibration is loaded)
|
|
20
|
+
* the Calibration tab. Either failure surfaces inline and stops; the
|
|
21
|
+
* dialog stays open so the operator can fix the input and retry.
|
|
22
|
+
*
|
|
23
|
+
* Sibling <CalibrationEntryDialog> still covers add-new-calibration
|
|
24
|
+
* and the in-place edit launched from the History table's pencil
|
|
25
|
+
* icon — this dialog overlaps the latter on purpose, since operators
|
|
26
|
+
* naturally expect calibration metadata to live alongside the asset
|
|
27
|
+
* they're editing.
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
30
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
|
21
31
|
import { Button } from 'primereact/button';
|
|
32
|
+
import { Calendar } from 'primereact/calendar';
|
|
22
33
|
import { Dialog } from 'primereact/dialog';
|
|
23
34
|
import { Dropdown } from 'primereact/dropdown';
|
|
24
35
|
import { InputText } from 'primereact/inputtext';
|
|
36
|
+
import { InputTextarea } from 'primereact/inputtextarea';
|
|
37
|
+
import { TabPanel, TabView } from 'primereact/tabview';
|
|
25
38
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
26
39
|
import { MessageType } from '../../hub/CommandMessage';
|
|
27
40
|
import { useAms, type AmsRole } from './AmsProvider';
|
|
@@ -70,6 +83,26 @@ function subLocationsFor(schemas: any, assetType: string): SubLocationsSchema |
|
|
|
70
83
|
return sl as SubLocationsSchema;
|
|
71
84
|
}
|
|
72
85
|
|
|
86
|
+
/** Flat top-level calibration_fields declared on the asset_type, if any.
|
|
87
|
+
* Empty array when the type uses the per-axis form or declares no
|
|
88
|
+
* calibration values at all. */
|
|
89
|
+
function calibrationFieldsFor(schemas: any, assetType: string): SchemaField[] {
|
|
90
|
+
const arr = schemas?.[assetType]?.calibration_fields;
|
|
91
|
+
return Array.isArray(arr) ? arr as SchemaField[] : [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Per-axis calibration_fields declared on the asset_type's
|
|
95
|
+
* sub_locations schema, if any. The asset_type uses *one* of:
|
|
96
|
+
* flat top-level `calibration_fields`, or per-axis
|
|
97
|
+
* `sub_locations.calibration_fields`. We render whichever is
|
|
98
|
+
* non-empty (top-level wins on collision since
|
|
99
|
+
* CalibrationEntryDialog handles them the same way). */
|
|
100
|
+
function perAxisCalibrationFieldsFor(
|
|
101
|
+
sub: SubLocationsSchema | null,
|
|
102
|
+
): SchemaField[] {
|
|
103
|
+
return (sub?.calibration_fields ?? []) as SchemaField[];
|
|
104
|
+
}
|
|
105
|
+
|
|
73
106
|
/** Coerce a per-cell string back to the field's declared type.
|
|
74
107
|
* Mirrors the Add dialog's helper so empty strings drop out, numbers
|
|
75
108
|
* come back as JSON numbers, bools as bools. */
|
|
@@ -102,7 +135,7 @@ function seedFromCustom(custom: Record<string, any> | undefined, name: string):
|
|
|
102
135
|
export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
103
136
|
visible, asset, onHide, onSaved,
|
|
104
137
|
}) => {
|
|
105
|
-
const { schemas, roles } = useAms();
|
|
138
|
+
const { schemas, roles, readCalibration } = useAms();
|
|
106
139
|
const { invoke } = useContext(EventEmitterContext);
|
|
107
140
|
|
|
108
141
|
const assetType = asset?.asset_type ?? '';
|
|
@@ -110,6 +143,19 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
110
143
|
const subLocationsSchema = useMemo(
|
|
111
144
|
() => subLocationsFor(schemas, assetType), [schemas, assetType],
|
|
112
145
|
);
|
|
146
|
+
const calibrationFields = useMemo(
|
|
147
|
+
() => calibrationFieldsFor(schemas, assetType), [schemas, assetType],
|
|
148
|
+
);
|
|
149
|
+
const perAxisCalibrationFields = useMemo(
|
|
150
|
+
() => perAxisCalibrationFieldsFor(subLocationsSchema), [subLocationsSchema],
|
|
151
|
+
);
|
|
152
|
+
/** Asset has per-axis calibration if its sub_locations schema
|
|
153
|
+
* declares calibration_fields. Otherwise it's flat (or has no
|
|
154
|
+
* cal values at all). Mirrors the rule CalibrationEntryDialog
|
|
155
|
+
* uses to pick its UI mode. */
|
|
156
|
+
const isPerAxisCalibration = perAxisCalibrationFields.length > 0;
|
|
157
|
+
const hasAnyCalibrationFields =
|
|
158
|
+
isPerAxisCalibration || calibrationFields.length > 0;
|
|
113
159
|
const rolesForType: AmsRole[] = useMemo(
|
|
114
160
|
() => (assetType ? roles[assetType] ?? [] : []),
|
|
115
161
|
[assetType, roles],
|
|
@@ -136,6 +182,30 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
136
182
|
const [submitting, setSubmitting] = useState(false);
|
|
137
183
|
const [error, setError] = useState<string | null>(null);
|
|
138
184
|
|
|
185
|
+
// Active tab. Defaults to the Asset tab on open. Stored in state
|
|
186
|
+
// so a parent re-render doesn't reset it while the operator is
|
|
187
|
+
// mid-edit on the Calibration tab.
|
|
188
|
+
const [activeTab, setActiveTab] = useState<number>(0);
|
|
189
|
+
|
|
190
|
+
// ── Calibration tab state ────────────────────────────────────────
|
|
191
|
+
// Loaded from `ams.read_calibration` when the dialog opens and the
|
|
192
|
+
// asset has a current calibration. `null` means "no current cal —
|
|
193
|
+
// tab is hidden." The operator can edit the meta fields and the
|
|
194
|
+
// values; on Save we POST `ams.update_calibration` with the
|
|
195
|
+
// current cal_id pinned. The server enforces that this matches
|
|
196
|
+
// the asset's current_calibration_id, so the tab only ever edits
|
|
197
|
+
// the cal currently in effect — older history is frozen.
|
|
198
|
+
const [calRecord, setCalRecord] = useState<any | null>(null);
|
|
199
|
+
const [calPerformedBy, setCalPerformedBy] = useState<string>('');
|
|
200
|
+
const [calExpiresAt, setCalExpiresAt] = useState<Date | null>(null);
|
|
201
|
+
const [calCertRef, setCalCertRef] = useState<string>('');
|
|
202
|
+
const [calNotes, setCalNotes] = useState<string>('');
|
|
203
|
+
/** Flat-schema cal values, e.g. `{ scale: 9.81, offset: 0 }`. */
|
|
204
|
+
const [calValues, setCalValues] = useState<Record<string, string>>({});
|
|
205
|
+
/** Per-axis cal matrix, `{ fx: { scale: "9.81", offset: "0" }, ... }`. */
|
|
206
|
+
const [calPerAxisValues, setCalPerAxisValues] =
|
|
207
|
+
useState<Record<string, Record<string, string>>>({});
|
|
208
|
+
|
|
139
209
|
// Seed every input from the asset when the dialog opens. Re-seeds
|
|
140
210
|
// on every open so editing one asset, cancelling, then opening
|
|
141
211
|
// another doesn't carry the previous values forward.
|
|
@@ -143,6 +213,7 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
143
213
|
if (!visible || !asset) return;
|
|
144
214
|
setError(null);
|
|
145
215
|
setSubmitting(false);
|
|
216
|
+
setActiveTab(0);
|
|
146
217
|
|
|
147
218
|
// Role: pick the dropdown option when the asset's location
|
|
148
219
|
// matches a declared role; otherwise route into ROLE_OTHER so
|
|
@@ -185,7 +256,66 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
185
256
|
}
|
|
186
257
|
}
|
|
187
258
|
setSubLocationFields(seededSub);
|
|
188
|
-
|
|
259
|
+
|
|
260
|
+
// ── Calibration tab: load the current cal record (if any)
|
|
261
|
+
// and seed every input from it. Wiped between opens by the
|
|
262
|
+
// initial setters below — handles "asset has no cal" and
|
|
263
|
+
// "edit a different asset" cleanly without leftover state.
|
|
264
|
+
setCalRecord(null);
|
|
265
|
+
setCalPerformedBy('');
|
|
266
|
+
setCalExpiresAt(null);
|
|
267
|
+
setCalCertRef('');
|
|
268
|
+
setCalNotes('');
|
|
269
|
+
setCalValues({});
|
|
270
|
+
setCalPerAxisValues({});
|
|
271
|
+
|
|
272
|
+
const calId = typeof asset.current_calibration_id === 'string'
|
|
273
|
+
? asset.current_calibration_id : '';
|
|
274
|
+
if (!calId) return;
|
|
275
|
+
|
|
276
|
+
let cancelled = false;
|
|
277
|
+
(async () => {
|
|
278
|
+
const rec = await readCalibration(asset.asset_id, calId);
|
|
279
|
+
if (cancelled || !rec) return;
|
|
280
|
+
setCalRecord(rec);
|
|
281
|
+
setCalPerformedBy(typeof rec.performed_by === 'string' ? rec.performed_by : '');
|
|
282
|
+
setCalCertRef(typeof rec.cert_ref === 'string' ? rec.cert_ref : '');
|
|
283
|
+
setCalNotes(typeof rec.notes === 'string' ? rec.notes : '');
|
|
284
|
+
setCalExpiresAt(rec.expires_at ? new Date(rec.expires_at) : null);
|
|
285
|
+
|
|
286
|
+
// Cal values come in the same shape we render. For a
|
|
287
|
+
// per-axis schema, we expect `values.<key>.<field>`; for
|
|
288
|
+
// a flat schema, `values.<field>` directly.
|
|
289
|
+
const values = (rec.values && typeof rec.values === 'object')
|
|
290
|
+
? rec.values as Record<string, any> : {};
|
|
291
|
+
if (isPerAxisCalibration && subLocationsSchema) {
|
|
292
|
+
const seededPa: Record<string, Record<string, string>> = {};
|
|
293
|
+
for (const key of subLocationsSchema.keys) {
|
|
294
|
+
const row = (values[key] && typeof values[key] === 'object')
|
|
295
|
+
? values[key] as Record<string, any> : {};
|
|
296
|
+
const r: Record<string, string> = {};
|
|
297
|
+
for (const f of perAxisCalibrationFields) {
|
|
298
|
+
const v = row[f.name];
|
|
299
|
+
r[f.name] = (v === undefined || v === null) ? '' : String(v);
|
|
300
|
+
}
|
|
301
|
+
seededPa[key] = r;
|
|
302
|
+
}
|
|
303
|
+
setCalPerAxisValues(seededPa);
|
|
304
|
+
} else {
|
|
305
|
+
const seededFlat: Record<string, string> = {};
|
|
306
|
+
for (const f of calibrationFields) {
|
|
307
|
+
const v = values[f.name];
|
|
308
|
+
seededFlat[f.name] = (v === undefined || v === null) ? '' : String(v);
|
|
309
|
+
}
|
|
310
|
+
setCalValues(seededFlat);
|
|
311
|
+
}
|
|
312
|
+
})();
|
|
313
|
+
return () => { cancelled = true; };
|
|
314
|
+
}, [
|
|
315
|
+
visible, asset, fields, subLocationsSchema, rolesForType,
|
|
316
|
+
calibrationFields, perAxisCalibrationFields, isPerAxisCalibration,
|
|
317
|
+
readCalibration,
|
|
318
|
+
]);
|
|
189
319
|
|
|
190
320
|
const onRoleChange = (value: string) => {
|
|
191
321
|
if (value === ROLE_OTHER) {
|
|
@@ -243,12 +373,41 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
243
373
|
const resp: any = await invoke(
|
|
244
374
|
'ams.update_asset' as any, MessageType.Request, payload,
|
|
245
375
|
);
|
|
246
|
-
if (resp?.success) {
|
|
247
|
-
onSaved?.();
|
|
248
|
-
onHide();
|
|
249
|
-
} else {
|
|
376
|
+
if (!resp?.success) {
|
|
250
377
|
setError(resp?.error_message ?? 'update_asset failed');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// If a calibration is loaded, save it too. Sequential is
|
|
382
|
+
// intentional — if the asset update succeeded but cal
|
|
383
|
+
// failed, the operator should retry the cal alone rather
|
|
384
|
+
// than re-submit the asset. The server enforces "cal_id
|
|
385
|
+
// must equal current_calibration_id", which guards against
|
|
386
|
+
// a stale dialog editing a cal that's since been replaced.
|
|
387
|
+
if (calRecord) {
|
|
388
|
+
const calPayload: any = {
|
|
389
|
+
asset_id: asset.asset_id,
|
|
390
|
+
cal_id: calRecord.cal_id,
|
|
391
|
+
performed_by: calPerformedBy,
|
|
392
|
+
expires_at: calExpiresAt ? calExpiresAt.toISOString() : null,
|
|
393
|
+
cert_ref: calCertRef,
|
|
394
|
+
notes: calNotes,
|
|
395
|
+
values: calValuesPayload(),
|
|
396
|
+
};
|
|
397
|
+
const calResp: any = await invoke(
|
|
398
|
+
'ams.update_calibration' as any, MessageType.Request, calPayload,
|
|
399
|
+
);
|
|
400
|
+
if (!calResp?.success) {
|
|
401
|
+
setError(calResp?.error_message ?? 'update_calibration failed');
|
|
402
|
+
// Switch the operator to the Calibration tab so
|
|
403
|
+
// they see the inputs that the error refers to.
|
|
404
|
+
setActiveTab(1);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
251
407
|
}
|
|
408
|
+
|
|
409
|
+
onSaved?.();
|
|
410
|
+
onHide();
|
|
252
411
|
} catch (e: any) {
|
|
253
412
|
setError(String(e?.message ?? e));
|
|
254
413
|
} finally {
|
|
@@ -256,6 +415,33 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
256
415
|
}
|
|
257
416
|
};
|
|
258
417
|
|
|
418
|
+
/** Build the `values` field for `ams.update_calibration`. Picks
|
|
419
|
+
* the per-axis matrix or the flat map based on the asset_type's
|
|
420
|
+
* schema; cells with empty input drop out so the on-disk record
|
|
421
|
+
* doesn't accumulate empty strings. */
|
|
422
|
+
const calValuesPayload = (): any => {
|
|
423
|
+
if (isPerAxisCalibration && subLocationsSchema) {
|
|
424
|
+
const out: Record<string, Record<string, any>> = {};
|
|
425
|
+
for (const key of subLocationsSchema.keys) {
|
|
426
|
+
const row: Record<string, any> = {};
|
|
427
|
+
for (const f of perAxisCalibrationFields) {
|
|
428
|
+
const raw = calPerAxisValues[key]?.[f.name] ?? '';
|
|
429
|
+
const coerced = coerceField(f, raw);
|
|
430
|
+
if (coerced !== undefined) row[f.name] = coerced;
|
|
431
|
+
}
|
|
432
|
+
out[key] = row;
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
const out: Record<string, any> = {};
|
|
437
|
+
for (const f of calibrationFields) {
|
|
438
|
+
const raw = calValues[f.name];
|
|
439
|
+
const coerced = coerceField(f, raw ?? '');
|
|
440
|
+
if (coerced !== undefined) out[f.name] = coerced;
|
|
441
|
+
}
|
|
442
|
+
return out;
|
|
443
|
+
};
|
|
444
|
+
|
|
259
445
|
// Gate Save on the same required-field rules the Add dialog uses,
|
|
260
446
|
// applied to the *current* (possibly edited) values. The server
|
|
261
447
|
// re-validates and we surface its error if anything slips through.
|
|
@@ -315,6 +501,8 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
315
501
|
)}
|
|
316
502
|
</div>
|
|
317
503
|
|
|
504
|
+
<TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
|
|
505
|
+
<TabPanel header="Asset">
|
|
318
506
|
<div style={{ display: 'grid',
|
|
319
507
|
gridTemplateColumns: 'auto 1fr',
|
|
320
508
|
gap: '0.5rem 1rem',
|
|
@@ -452,6 +640,155 @@ export const AssetEditDialog: React.FC<AssetEditDialogProps> = ({
|
|
|
452
640
|
</div>
|
|
453
641
|
</div>
|
|
454
642
|
)}
|
|
643
|
+
</TabPanel>
|
|
644
|
+
|
|
645
|
+
{/* Calibration tab — present only when the asset has
|
|
646
|
+
a current calibration. We hide the tab entirely
|
|
647
|
+
rather than showing it disabled because operators
|
|
648
|
+
asked for the affordance to be visually obvious
|
|
649
|
+
when it's available, not a permanent grey tease
|
|
650
|
+
of "you can't edit this yet". Adding a first cal
|
|
651
|
+
flows through `+ Calibration` on the parent. */}
|
|
652
|
+
{calRecord && (
|
|
653
|
+
<TabPanel header={`Calibration (${calRecord.cal_id})`}>
|
|
654
|
+
{/* Meta fields — performed_by, expires_at,
|
|
655
|
+
cert_ref, notes. Same shape as the entry
|
|
656
|
+
dialog so operators see consistent inputs
|
|
657
|
+
either way they reach the cal form. */}
|
|
658
|
+
<div style={{ display: 'grid',
|
|
659
|
+
gridTemplateColumns: 'auto 1fr',
|
|
660
|
+
gap: '0.5rem 1rem',
|
|
661
|
+
alignItems: 'center' }}>
|
|
662
|
+
<label>Performed by</label>
|
|
663
|
+
<InputText value={calPerformedBy}
|
|
664
|
+
onChange={(e) => setCalPerformedBy(e.target.value)} />
|
|
665
|
+
|
|
666
|
+
<label>Expires at</label>
|
|
667
|
+
<Calendar value={calExpiresAt}
|
|
668
|
+
onChange={(e) => setCalExpiresAt((e.value as Date) ?? null)}
|
|
669
|
+
showIcon dateFormat="yy-mm-dd" />
|
|
670
|
+
|
|
671
|
+
<label>Certificate ref</label>
|
|
672
|
+
<InputText value={calCertRef}
|
|
673
|
+
onChange={(e) => setCalCertRef(e.target.value)}
|
|
674
|
+
placeholder="e.g. ADC-CAL-2026-1043"
|
|
675
|
+
/>
|
|
676
|
+
|
|
677
|
+
<label>Notes</label>
|
|
678
|
+
<InputTextarea value={calNotes} rows={3}
|
|
679
|
+
onChange={(e) => setCalNotes(e.target.value)}
|
|
680
|
+
autoResize />
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
{/* Cal values. Per-axis when the schema
|
|
684
|
+
declares them; otherwise the flat field
|
|
685
|
+
list. Skipped entirely when the asset_type
|
|
686
|
+
declares no calibration_fields — the meta
|
|
687
|
+
fields above still let the operator fix
|
|
688
|
+
cert_ref / notes / expiry on a values-less
|
|
689
|
+
calibration. */}
|
|
690
|
+
{hasAnyCalibrationFields && (
|
|
691
|
+
<div style={{ marginTop: '1rem' }}>
|
|
692
|
+
<h4 style={{ margin: '0 0 0.5rem 0' }}>
|
|
693
|
+
{isPerAxisCalibration
|
|
694
|
+
? (subLocationsSchema?.label ?? 'Per-axis calibration')
|
|
695
|
+
: 'Calibration values'}
|
|
696
|
+
</h4>
|
|
697
|
+
{isPerAxisCalibration && subLocationsSchema ? (
|
|
698
|
+
<div style={{ overflowX: 'auto' }}>
|
|
699
|
+
<table style={{ width: '100%', borderCollapse: 'collapse',
|
|
700
|
+
fontSize: '0.875rem' }}>
|
|
701
|
+
<thead>
|
|
702
|
+
<tr>
|
|
703
|
+
<th style={{ textAlign: 'left',
|
|
704
|
+
padding: '0.25rem 0.5rem',
|
|
705
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
706
|
+
{subLocationsSchema.key_label ?? 'Key'}
|
|
707
|
+
</th>
|
|
708
|
+
{perAxisCalibrationFields.map(f => (
|
|
709
|
+
<th key={f.name}
|
|
710
|
+
title={f.description ?? undefined}
|
|
711
|
+
style={{ textAlign: 'left',
|
|
712
|
+
padding: '0.25rem 0.5rem',
|
|
713
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
714
|
+
{(f.label ?? f.name)}
|
|
715
|
+
{f.units ? ` [${f.units}]` : ''}
|
|
716
|
+
{f.required ? ' *' : ''}
|
|
717
|
+
</th>
|
|
718
|
+
))}
|
|
719
|
+
</tr>
|
|
720
|
+
</thead>
|
|
721
|
+
<tbody>
|
|
722
|
+
{subLocationsSchema.keys.map(key => (
|
|
723
|
+
<tr key={key}>
|
|
724
|
+
<td style={{ padding: '0.25rem 0.5rem', fontWeight: 600 }}>
|
|
725
|
+
{key}
|
|
726
|
+
</td>
|
|
727
|
+
{perAxisCalibrationFields.map(field => {
|
|
728
|
+
const cellValue =
|
|
729
|
+
calPerAxisValues[key]?.[field.name] ?? '';
|
|
730
|
+
const isNum =
|
|
731
|
+
field.type !== 'string' && field.type !== 'bool';
|
|
732
|
+
return (
|
|
733
|
+
<td key={field.name}
|
|
734
|
+
style={{ padding: '0.25rem 0.5rem' }}>
|
|
735
|
+
<InputText
|
|
736
|
+
value={cellValue}
|
|
737
|
+
keyfilter={isNum ? 'num' : undefined}
|
|
738
|
+
onChange={(e) => {
|
|
739
|
+
const v = e.target.value;
|
|
740
|
+
setCalPerAxisValues(s => ({
|
|
741
|
+
...s,
|
|
742
|
+
[key]: {
|
|
743
|
+
...(s[key] ?? {}),
|
|
744
|
+
[field.name]: v,
|
|
745
|
+
},
|
|
746
|
+
}));
|
|
747
|
+
}}
|
|
748
|
+
style={{ width: '100%' }}
|
|
749
|
+
/>
|
|
750
|
+
</td>
|
|
751
|
+
);
|
|
752
|
+
})}
|
|
753
|
+
</tr>
|
|
754
|
+
))}
|
|
755
|
+
</tbody>
|
|
756
|
+
</table>
|
|
757
|
+
</div>
|
|
758
|
+
) : (
|
|
759
|
+
<div style={{ display: 'grid',
|
|
760
|
+
gridTemplateColumns: 'auto 1fr',
|
|
761
|
+
gap: '0.5rem 1rem',
|
|
762
|
+
alignItems: 'center' }}>
|
|
763
|
+
{calibrationFields.map(field => {
|
|
764
|
+
const label = field.label ?? field.name;
|
|
765
|
+
const fullLabel = field.units
|
|
766
|
+
? `${label} [${field.units}]${field.required ? ' *' : ''}`
|
|
767
|
+
: `${label}${field.required ? ' *' : ''}`;
|
|
768
|
+
const isNum = field.type !== 'string'
|
|
769
|
+
&& field.type !== 'bool';
|
|
770
|
+
return (
|
|
771
|
+
<React.Fragment key={field.name}>
|
|
772
|
+
<label title={field.description ?? undefined}>
|
|
773
|
+
{fullLabel}
|
|
774
|
+
</label>
|
|
775
|
+
<InputText
|
|
776
|
+
value={calValues[field.name] ?? ''}
|
|
777
|
+
keyfilter={isNum ? 'num' : undefined}
|
|
778
|
+
onChange={(e) => setCalValues(s => ({
|
|
779
|
+
...s, [field.name]: e.target.value,
|
|
780
|
+
}))}
|
|
781
|
+
/>
|
|
782
|
+
</React.Fragment>
|
|
783
|
+
);
|
|
784
|
+
})}
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
)}
|
|
789
|
+
</TabPanel>
|
|
790
|
+
)}
|
|
791
|
+
</TabView>
|
|
455
792
|
|
|
456
793
|
{error && (
|
|
457
794
|
<div style={{ marginTop: '1rem', color: '#ef4444' }}>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormRow — grid-aligned label/input pair with optional required marker
|
|
3
|
+
* and inline error text. Drop inside a FormSection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import './forms.css';
|
|
8
|
+
|
|
9
|
+
export interface FormRowProps {
|
|
10
|
+
label: React.ReactNode;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
/** Optional descriptive hint shown beneath the label. */
|
|
13
|
+
hint?: React.ReactNode;
|
|
14
|
+
/** Inline error text shown beneath the input. */
|
|
15
|
+
error?: React.ReactNode;
|
|
16
|
+
/** Optional `htmlFor` association on the label tag. */
|
|
17
|
+
htmlFor?: string;
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const FormRow: React.FC<FormRowProps> = ({ label, required, hint, error, htmlFor, children }) => {
|
|
22
|
+
return (
|
|
23
|
+
<div className={`ac-form-row${error ? ' ac-form-row--error' : ''}`}>
|
|
24
|
+
<label className="ac-form-row__label" htmlFor={htmlFor}>
|
|
25
|
+
{label}
|
|
26
|
+
{required && <span className="ac-form-row__required" aria-hidden> *</span>}
|
|
27
|
+
{hint && <small className="ac-form-row__hint">{hint}</small>}
|
|
28
|
+
</label>
|
|
29
|
+
<div className="ac-form-row__field">
|
|
30
|
+
{children}
|
|
31
|
+
{error && <small className="ac-form-row__error">{error}</small>}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default FormRow;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormSection — labeled bordered region containing a group of FormRow rows.
|
|
3
|
+
*
|
|
4
|
+
* Used by the TIS editor subforms and intended for app-level settings
|
|
5
|
+
* pages. Replaces the ad-hoc `.ac-form-grid` divs scattered across project
|
|
6
|
+
* settings views.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import './forms.css';
|
|
11
|
+
|
|
12
|
+
export interface FormSectionProps {
|
|
13
|
+
title?: React.ReactNode;
|
|
14
|
+
description?: React.ReactNode;
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
/** Right-aligned controls (buttons, dirty pill, etc.). */
|
|
17
|
+
actions?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const FormSection: React.FC<FormSectionProps> = ({ title, description, actions, children }) => {
|
|
21
|
+
return (
|
|
22
|
+
<section className="ac-form-section">
|
|
23
|
+
{(title || actions) && (
|
|
24
|
+
<header className="ac-form-section__header">
|
|
25
|
+
<div>
|
|
26
|
+
{title && <h3 className="ac-form-section__title">{title}</h3>}
|
|
27
|
+
{description && <small className="ac-form-section__desc">{description}</small>}
|
|
28
|
+
</div>
|
|
29
|
+
{actions && <div className="ac-form-section__actions">{actions}</div>}
|
|
30
|
+
</header>
|
|
31
|
+
)}
|
|
32
|
+
<div className="ac-form-section__body">
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default FormSection;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
.ac-form-section {
|
|
2
|
+
border: 1px solid var(--surface-d, #e2e8f0);
|
|
3
|
+
border-radius: 6px;
|
|
4
|
+
background: var(--surface-card, #fff);
|
|
5
|
+
margin-bottom: 1rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.ac-form-section__header {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: flex-start;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
padding: 0.5rem 1rem;
|
|
13
|
+
border-bottom: 1px solid var(--surface-d, #e2e8f0);
|
|
14
|
+
background: var(--surface-b, #f8fafc);
|
|
15
|
+
border-top-left-radius: 6px;
|
|
16
|
+
border-top-right-radius: 6px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ac-form-section__title {
|
|
20
|
+
margin: 0;
|
|
21
|
+
font-size: 0.95rem;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ac-form-section__desc {
|
|
26
|
+
color: var(--text-color-secondary, #64748b);
|
|
27
|
+
display: block;
|
|
28
|
+
margin-top: 0.15rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ac-form-section__actions {
|
|
32
|
+
display: flex;
|
|
33
|
+
gap: 0.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ac-form-section__body {
|
|
37
|
+
padding: 0.75rem 1rem;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 0.5rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ac-form-row {
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: minmax(8rem, 14rem) 1fr;
|
|
46
|
+
gap: 0.5rem 1rem;
|
|
47
|
+
align-items: start;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ac-form-row--error .ac-form-row__field input,
|
|
51
|
+
.ac-form-row--error .ac-form-row__field .p-inputtext {
|
|
52
|
+
border-color: #dc2626;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ac-form-row__label {
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
padding-top: 0.4rem;
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ac-form-row__required {
|
|
63
|
+
color: #dc2626;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ac-form-row__hint {
|
|
67
|
+
color: var(--text-color-secondary, #64748b);
|
|
68
|
+
font-weight: 400;
|
|
69
|
+
font-size: 0.75rem;
|
|
70
|
+
margin-top: 0.15rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ac-form-row__field {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 0.25rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ac-form-row__field > input,
|
|
80
|
+
.ac-form-row__field > .p-inputtext,
|
|
81
|
+
.ac-form-row__field > .p-dropdown,
|
|
82
|
+
.ac-form-row__field > .p-inputtextarea {
|
|
83
|
+
width: 100%;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ac-form-row__error {
|
|
87
|
+
color: #dc2626;
|
|
88
|
+
font-size: 0.75rem;
|
|
89
|
+
}
|