@adcops/autocore-react 3.3.59 → 3.3.63
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/ams/AmsProvider.d.ts +45 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -0
- package/dist/components/ams/AmsProvider.js +1 -0
- package/dist/components/ams/AssetDetailView.d.ts +3 -0
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -0
- package/dist/components/ams/AssetDetailView.js +1 -0
- package/dist/components/ams/AssetRegistryTable.d.ts +3 -0
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -0
- package/dist/components/ams/AssetRegistryTable.js +1 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts +10 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -0
- package/dist/components/ams/CalibrationEntryDialog.js +1 -0
- package/dist/components/ams/SubLocationPicker.d.ts +3 -0
- package/dist/components/ams/SubLocationPicker.d.ts.map +1 -0
- package/dist/components/ams/SubLocationPicker.js +1 -0
- package/dist/components/ams/index.d.ts +6 -0
- package/dist/components/ams/index.d.ts.map +1 -0
- package/dist/components/ams/index.js +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ProjectSelector.d.ts +15 -0
- package/dist/components/tis/ProjectSelector.d.ts.map +1 -0
- package/dist/components/tis/ProjectSelector.js +1 -0
- package/dist/components/tis/TestDataView.d.ts +9 -1
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +8 -4
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +45 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/core/AutoCoreTagContext.d.ts +16 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +67 -37
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +1 -1
- package/src/components/ams/AmsProvider.tsx +219 -0
- package/src/components/ams/AssetDetailView.tsx +101 -0
- package/src/components/ams/AssetRegistryTable.tsx +171 -0
- package/src/components/ams/CalibrationEntryDialog.tsx +197 -0
- package/src/components/ams/SubLocationPicker.tsx +146 -0
- package/src/components/ams/index.ts +12 -0
- package/src/components/index.ts +30 -0
- package/src/components/tis/ProjectSelector.tsx +190 -0
- package/src/components/tis/TestDataView.tsx +321 -28
- package/src/components/tis/TestSetupForm.tsx +66 -253
- package/src/components/tis/TisProvider.tsx +192 -1
- package/src/core/AutoCoreTagContext.tsx +114 -16
- package/src/themes/adc-dark/_extensions.scss +15 -0
- package/src/themes/adc-dark/blue/adc_theme.scss +56 -10
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <AssetRegistryTable> — list every asset in the AMS, with quick
|
|
3
|
+
* filters by type / status and a "+ Add" button that pops a creation
|
|
4
|
+
* dialog. Click a row → pins selection.assetId so <AssetDetailView>
|
|
5
|
+
* can render the chosen asset.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useContext, useMemo, useState } from 'react';
|
|
9
|
+
import { Button } from 'primereact/button';
|
|
10
|
+
import { DataTable } from 'primereact/datatable';
|
|
11
|
+
import { Column } from 'primereact/column';
|
|
12
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
13
|
+
import { InputText } from 'primereact/inputtext';
|
|
14
|
+
import { Dialog } from 'primereact/dialog';
|
|
15
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
16
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
17
|
+
import { useAms, type AmsAssetEntry } from './AmsProvider';
|
|
18
|
+
|
|
19
|
+
interface AddDialogState {
|
|
20
|
+
open: boolean;
|
|
21
|
+
assetType: string;
|
|
22
|
+
serial: string;
|
|
23
|
+
location: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EMPTY_ADD: AddDialogState = { open: false, assetType: '', serial: '', location: '' };
|
|
27
|
+
|
|
28
|
+
export const AssetRegistryTable: React.FC = () => {
|
|
29
|
+
const { schemas, assets, refreshAssets, setSelection, selection } = useAms();
|
|
30
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
31
|
+
|
|
32
|
+
const [filterType, setFilterType] = useState<string | null>(null);
|
|
33
|
+
const [filterStatus, setFilterStatus] = useState<string | null>(null);
|
|
34
|
+
const [addState, setAddState] = useState<AddDialogState>(EMPTY_ADD);
|
|
35
|
+
|
|
36
|
+
const typeOptions = useMemo(
|
|
37
|
+
() => Object.keys(schemas).map(k => ({ label: schemas[k]?.label ?? k, value: k })),
|
|
38
|
+
[schemas],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const filtered = useMemo(() => {
|
|
42
|
+
return assets.filter(a => {
|
|
43
|
+
if (filterType && a.asset_type !== filterType) return false;
|
|
44
|
+
if (filterStatus && a.status !== filterStatus) return false;
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
}, [assets, filterType, filterStatus]);
|
|
48
|
+
|
|
49
|
+
const onCreate = async () => {
|
|
50
|
+
if (!addState.assetType) return;
|
|
51
|
+
try {
|
|
52
|
+
const resp: any = await invoke('ams.create_asset' as any, MessageType.Request, {
|
|
53
|
+
asset_type: addState.assetType,
|
|
54
|
+
serial: addState.serial,
|
|
55
|
+
location: addState.location,
|
|
56
|
+
} as any);
|
|
57
|
+
if (resp?.success) {
|
|
58
|
+
setAddState(EMPTY_ADD);
|
|
59
|
+
await refreshAssets();
|
|
60
|
+
if (resp.data?.asset_id) {
|
|
61
|
+
setSelection({ assetType: addState.assetType, assetId: resp.data.asset_id });
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
console.warn('[AssetRegistryTable] create_asset failed:', resp?.error_message);
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('[AssetRegistryTable] create_asset threw:', e);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
73
|
+
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
|
|
74
|
+
<Dropdown
|
|
75
|
+
value={filterType}
|
|
76
|
+
options={[{ label: 'All Types', value: null }, ...typeOptions]}
|
|
77
|
+
onChange={(e) => setFilterType(e.value)}
|
|
78
|
+
placeholder="Filter by type"
|
|
79
|
+
/>
|
|
80
|
+
<Dropdown
|
|
81
|
+
value={filterStatus}
|
|
82
|
+
options={[
|
|
83
|
+
{ label: 'All Statuses', value: null },
|
|
84
|
+
{ label: 'Active', value: 'active' },
|
|
85
|
+
{ label: 'Out for Service', value: 'out_for_service' },
|
|
86
|
+
{ label: 'Retired', value: 'retired' },
|
|
87
|
+
]}
|
|
88
|
+
onChange={(e) => setFilterStatus(e.value)}
|
|
89
|
+
placeholder="Filter by status"
|
|
90
|
+
/>
|
|
91
|
+
<span style={{ marginLeft: 'auto' }}>
|
|
92
|
+
<Button
|
|
93
|
+
icon="pi pi-plus"
|
|
94
|
+
label="Add Asset"
|
|
95
|
+
onClick={() => setAddState(s => ({ ...s, open: true }))}
|
|
96
|
+
disabled={typeOptions.length === 0}
|
|
97
|
+
/>
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<DataTable
|
|
102
|
+
value={filtered}
|
|
103
|
+
selectionMode="single"
|
|
104
|
+
selection={filtered.find(a => a.asset_id === selection.assetId) ?? null}
|
|
105
|
+
onSelectionChange={(e) => {
|
|
106
|
+
const row = e.value as AmsAssetEntry | null;
|
|
107
|
+
if (row) setSelection({ assetType: row.asset_type, assetId: row.asset_id });
|
|
108
|
+
}}
|
|
109
|
+
dataKey="asset_id"
|
|
110
|
+
emptyMessage={
|
|
111
|
+
typeOptions.length === 0
|
|
112
|
+
? 'AMS not enabled in this project (no asset_types declared).'
|
|
113
|
+
: 'No assets registered yet.'
|
|
114
|
+
}
|
|
115
|
+
size="small"
|
|
116
|
+
stripedRows
|
|
117
|
+
>
|
|
118
|
+
<Column field="asset_id" header="Asset ID" />
|
|
119
|
+
<Column field="asset_type" header="Type"
|
|
120
|
+
body={(r: AmsAssetEntry) => schemas[r.asset_type]?.label ?? r.asset_type}
|
|
121
|
+
/>
|
|
122
|
+
<Column field="serial" header="Serial" />
|
|
123
|
+
<Column field="location" header="Location" />
|
|
124
|
+
<Column field="status" header="Status" />
|
|
125
|
+
<Column header="Calibration"
|
|
126
|
+
body={(r: AmsAssetEntry) =>
|
|
127
|
+
r.current_calibration_id
|
|
128
|
+
? <span title={r.current_calibration_id}>✓</span>
|
|
129
|
+
: <span style={{ color: '#f59e0b' }}>none</span>
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
</DataTable>
|
|
133
|
+
|
|
134
|
+
<Dialog
|
|
135
|
+
header="Add New Asset"
|
|
136
|
+
visible={addState.open}
|
|
137
|
+
style={{ width: '32rem' }}
|
|
138
|
+
onHide={() => setAddState(EMPTY_ADD)}
|
|
139
|
+
footer={
|
|
140
|
+
<>
|
|
141
|
+
<Button label="Cancel" severity="secondary" onClick={() => setAddState(EMPTY_ADD)} />
|
|
142
|
+
<Button label="Create" icon="pi pi-check"
|
|
143
|
+
onClick={onCreate}
|
|
144
|
+
disabled={!addState.assetType}
|
|
145
|
+
/>
|
|
146
|
+
</>
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '0.5rem 1rem', alignItems: 'center' }}>
|
|
150
|
+
<label>Type *</label>
|
|
151
|
+
<Dropdown
|
|
152
|
+
value={addState.assetType}
|
|
153
|
+
options={typeOptions}
|
|
154
|
+
onChange={(e) => setAddState(s => ({ ...s, assetType: e.value }))}
|
|
155
|
+
placeholder="Choose asset type"
|
|
156
|
+
/>
|
|
157
|
+
<label>Serial</label>
|
|
158
|
+
<InputText value={addState.serial}
|
|
159
|
+
onChange={(e) => setAddState(s => ({ ...s, serial: e.target.value }))} />
|
|
160
|
+
<label>Location</label>
|
|
161
|
+
<InputText value={addState.location}
|
|
162
|
+
onChange={(e) => setAddState(s => ({ ...s, location: e.target.value }))} />
|
|
163
|
+
</div>
|
|
164
|
+
<p style={{ fontSize: '0.875rem', color: '#9ca3af', marginTop: '1rem' }}>
|
|
165
|
+
The asset_id is generated by the server (format: <code>{addState.assetType ? (schemas[addState.assetType]?.id_prefix ?? 'A-') : 'A-'}YYYYMMDDTHHMMSS</code>).
|
|
166
|
+
Manufacturer serial is recorded for traceability but is not used as the unique key.
|
|
167
|
+
</p>
|
|
168
|
+
</Dialog>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <CalibrationEntryDialog> — form for adding a calibration record to
|
|
3
|
+
* an asset. Renders a dynamic field set built from the asset_type's
|
|
4
|
+
* `calibration_fields` schema, plus a few standard meta fields.
|
|
5
|
+
*
|
|
6
|
+
* Triggered by <AssetDetailView>'s "+ Calibration" button. Submits via
|
|
7
|
+
* `ams.add_calibration`; on success the AmsProvider's
|
|
8
|
+
* `ams.calibration_added` subscription refreshes the registry.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
12
|
+
import { Button } from 'primereact/button';
|
|
13
|
+
import { Dialog } from 'primereact/dialog';
|
|
14
|
+
import { InputText } from 'primereact/inputtext';
|
|
15
|
+
import { InputNumber } from 'primereact/inputnumber';
|
|
16
|
+
import { Calendar } from 'primereact/calendar';
|
|
17
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
18
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
19
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
20
|
+
import { useAmsSchemas } from './AmsProvider';
|
|
21
|
+
|
|
22
|
+
export interface CalibrationEntryDialogProps {
|
|
23
|
+
visible: boolean;
|
|
24
|
+
assetId: string;
|
|
25
|
+
assetType: string;
|
|
26
|
+
onHide: () => void;
|
|
27
|
+
onAdded?: (calId: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FieldSpec {
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
required?: boolean;
|
|
34
|
+
label?: string;
|
|
35
|
+
units?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
values?: string[]; // for enum
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const CalibrationEntryDialog: React.FC<CalibrationEntryDialogProps> = ({
|
|
41
|
+
visible, assetId, assetType, onHide, onAdded,
|
|
42
|
+
}) => {
|
|
43
|
+
const schemas = useAmsSchemas();
|
|
44
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
45
|
+
|
|
46
|
+
const fields: FieldSpec[] = (schemas[assetType]?.calibration_fields ?? []) as FieldSpec[];
|
|
47
|
+
|
|
48
|
+
const [values, setValues] = useState<{ [k: string]: any }>({});
|
|
49
|
+
const [performedBy, setPerformedBy] = useState('');
|
|
50
|
+
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
|
|
51
|
+
const [certRef, setCertRef] = useState('');
|
|
52
|
+
const [notes, setNotes] = useState('');
|
|
53
|
+
const [submitting, setSubmitting] = useState(false);
|
|
54
|
+
const [error, setError] = useState<string | null>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (visible) {
|
|
58
|
+
setValues({});
|
|
59
|
+
setPerformedBy('');
|
|
60
|
+
setExpiresAt(null);
|
|
61
|
+
setCertRef('');
|
|
62
|
+
setNotes('');
|
|
63
|
+
setError(null);
|
|
64
|
+
}
|
|
65
|
+
}, [visible]);
|
|
66
|
+
|
|
67
|
+
const renderField = (f: FieldSpec) => {
|
|
68
|
+
const labelText = `${f.label ?? f.name}${f.units ? ` [${f.units}]` : ''}${f.required ? ' *' : ''}`;
|
|
69
|
+
const labelEl = (
|
|
70
|
+
<label title={f.description ?? ''}>
|
|
71
|
+
{labelText}
|
|
72
|
+
</label>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const onChange = (val: any) => setValues(v => ({ ...v, [f.name]: val }));
|
|
76
|
+
|
|
77
|
+
let input: React.ReactNode;
|
|
78
|
+
switch (f.type) {
|
|
79
|
+
case 'string':
|
|
80
|
+
input = <InputText value={values[f.name] ?? ''} onChange={(e) => onChange(e.target.value)} />;
|
|
81
|
+
break;
|
|
82
|
+
case 'enum':
|
|
83
|
+
input = (
|
|
84
|
+
<Dropdown
|
|
85
|
+
value={values[f.name] ?? null}
|
|
86
|
+
options={(f.values ?? []).map(v => ({ label: v, value: v }))}
|
|
87
|
+
onChange={(e) => onChange(e.value)}
|
|
88
|
+
placeholder="Choose…"
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
break;
|
|
92
|
+
case 'bool':
|
|
93
|
+
input = (
|
|
94
|
+
<Dropdown
|
|
95
|
+
value={values[f.name] ?? null}
|
|
96
|
+
options={[{ label: 'true', value: true }, { label: 'false', value: false }]}
|
|
97
|
+
onChange={(e) => onChange(e.value)}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
break;
|
|
101
|
+
case 'u8': case 'u16': case 'u32': case 'u64':
|
|
102
|
+
case 'i8': case 'i16': case 'i32': case 'i64':
|
|
103
|
+
case 'f32': case 'f64':
|
|
104
|
+
input = (
|
|
105
|
+
<InputNumber
|
|
106
|
+
value={values[f.name] ?? null}
|
|
107
|
+
onValueChange={(e) => onChange(e.value)}
|
|
108
|
+
useGrouping={false}
|
|
109
|
+
maxFractionDigits={f.type.startsWith('f') ? 9 : 0}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
input = <InputText value={values[f.name] ?? ''} onChange={(e) => onChange(e.target.value)} />;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
return [labelEl, input] as const;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const onSubmit = async () => {
|
|
121
|
+
// Basic required-field check.
|
|
122
|
+
for (const f of fields) {
|
|
123
|
+
if (f.required && (values[f.name] === undefined || values[f.name] === null || values[f.name] === '')) {
|
|
124
|
+
setError(`Field "${f.label ?? f.name}" is required.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
setSubmitting(true);
|
|
129
|
+
setError(null);
|
|
130
|
+
try {
|
|
131
|
+
const resp: any = await invoke('ams.add_calibration' as any, MessageType.Request, {
|
|
132
|
+
asset_id: assetId,
|
|
133
|
+
performed_by: performedBy,
|
|
134
|
+
expires_at: expiresAt ? expiresAt.toISOString() : null,
|
|
135
|
+
cert_ref: certRef,
|
|
136
|
+
notes,
|
|
137
|
+
values,
|
|
138
|
+
} as any);
|
|
139
|
+
if (resp?.success) {
|
|
140
|
+
onAdded?.(resp.data.cal_id);
|
|
141
|
+
onHide();
|
|
142
|
+
} else {
|
|
143
|
+
setError(resp?.error_message ?? 'add_calibration failed');
|
|
144
|
+
}
|
|
145
|
+
} catch (e: any) {
|
|
146
|
+
setError(String(e?.message ?? e));
|
|
147
|
+
} finally {
|
|
148
|
+
setSubmitting(false);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Dialog
|
|
154
|
+
header={`Add Calibration — ${assetId}`}
|
|
155
|
+
visible={visible}
|
|
156
|
+
onHide={onHide}
|
|
157
|
+
style={{ width: '40rem' }}
|
|
158
|
+
footer={
|
|
159
|
+
<>
|
|
160
|
+
<Button label="Cancel" severity="secondary" onClick={onHide} disabled={submitting} />
|
|
161
|
+
<Button label="Save" icon="pi pi-check" onClick={onSubmit} loading={submitting} />
|
|
162
|
+
</>
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '0.5rem 1rem', alignItems: 'center' }}>
|
|
166
|
+
{fields.flatMap((f, i) => {
|
|
167
|
+
const [l, inp] = renderField(f);
|
|
168
|
+
return [
|
|
169
|
+
<React.Fragment key={`l-${i}`}>{l}</React.Fragment>,
|
|
170
|
+
<React.Fragment key={`i-${i}`}>{inp}</React.Fragment>,
|
|
171
|
+
];
|
|
172
|
+
})}
|
|
173
|
+
|
|
174
|
+
<hr style={{ gridColumn: '1 / span 2', width: '100%' }} />
|
|
175
|
+
|
|
176
|
+
<label>Performed by</label>
|
|
177
|
+
<InputText value={performedBy} onChange={(e) => setPerformedBy(e.target.value)} />
|
|
178
|
+
|
|
179
|
+
<label>Expires at</label>
|
|
180
|
+
<Calendar value={expiresAt} onChange={(e) => setExpiresAt((e.value as Date) ?? null)}
|
|
181
|
+
showIcon dateFormat="yy-mm-dd" />
|
|
182
|
+
|
|
183
|
+
<label>Certificate ref</label>
|
|
184
|
+
<InputText value={certRef} onChange={(e) => setCertRef(e.target.value)} />
|
|
185
|
+
|
|
186
|
+
<label>Notes</label>
|
|
187
|
+
<InputText value={notes} onChange={(e) => setNotes(e.target.value)} />
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{error && (
|
|
191
|
+
<div style={{ marginTop: '1rem', color: '#ef4444' }}>
|
|
192
|
+
{error}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</Dialog>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <SubLocationPicker> — grid view for an asset's sub_locations
|
|
3
|
+
* (typically surface lanes). Each cell shows the sub_location's status
|
|
4
|
+
* and lets the operator click to mark it in_use / worn / available, or
|
|
5
|
+
* select it for the active test.
|
|
6
|
+
*
|
|
7
|
+
* Pure context-driven: reads selection.assetId from <AmsProvider>.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
11
|
+
import { Button } from 'primereact/button';
|
|
12
|
+
import { Dialog } from 'primereact/dialog';
|
|
13
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
14
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
15
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
16
|
+
import { useAms } from './AmsProvider';
|
|
17
|
+
|
|
18
|
+
interface SubLocationItem {
|
|
19
|
+
id: string;
|
|
20
|
+
[k: string]: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SubLocations {
|
|
24
|
+
name: string;
|
|
25
|
+
items: SubLocationItem[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SubLocationPicker: React.FC = () => {
|
|
29
|
+
const { selection } = useAms();
|
|
30
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
31
|
+
const [subs, setSubs] = useState<SubLocations | null>(null);
|
|
32
|
+
const [editing, setEditing] = useState<{ id: string; status: string } | null>(null);
|
|
33
|
+
|
|
34
|
+
const refresh = async () => {
|
|
35
|
+
if (!selection.assetId) {
|
|
36
|
+
setSubs(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const resp: any = await invoke('ams.list_sub_locations' as any, MessageType.Request, {
|
|
41
|
+
asset_id: selection.assetId,
|
|
42
|
+
} as any);
|
|
43
|
+
if (resp?.success) {
|
|
44
|
+
const sl = resp.data?.sub_locations;
|
|
45
|
+
if (sl && Array.isArray(sl.items)) {
|
|
46
|
+
setSubs(sl as SubLocations);
|
|
47
|
+
} else {
|
|
48
|
+
setSubs(null);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error('[SubLocationPicker] list_sub_locations:', e);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
useEffect(() => { refresh(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [selection.assetId]);
|
|
56
|
+
|
|
57
|
+
if (!selection.assetId) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!subs) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const onSave = async () => {
|
|
65
|
+
if (!editing) return;
|
|
66
|
+
try {
|
|
67
|
+
await invoke('ams.update_sub_location' as any, MessageType.Request, {
|
|
68
|
+
asset_id: selection.assetId!,
|
|
69
|
+
location_id: editing.id,
|
|
70
|
+
partial: { status: editing.status },
|
|
71
|
+
} as any);
|
|
72
|
+
setEditing(null);
|
|
73
|
+
await refresh();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('[SubLocationPicker] update:', e);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const colorFor = (status: string): string => {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case 'available': return '#22c55e';
|
|
82
|
+
case 'in_use': return '#3b82f6';
|
|
83
|
+
case 'worn': return '#f59e0b';
|
|
84
|
+
case 'retired': return '#6b7280';
|
|
85
|
+
default: return '#9ca3af';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<h4 style={{ marginBottom: '0.5rem', textTransform: 'capitalize' }}>{subs.name}</h4>
|
|
92
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(7rem, 1fr))', gap: '0.5rem' }}>
|
|
93
|
+
{subs.items.map(item => {
|
|
94
|
+
const status = String(item.status ?? '');
|
|
95
|
+
return (
|
|
96
|
+
<button
|
|
97
|
+
key={item.id}
|
|
98
|
+
onClick={() => setEditing({ id: item.id, status })}
|
|
99
|
+
style={{
|
|
100
|
+
padding: '0.75rem',
|
|
101
|
+
border: `2px solid ${colorFor(status)}`,
|
|
102
|
+
borderRadius: 6,
|
|
103
|
+
background: 'transparent',
|
|
104
|
+
color: 'inherit',
|
|
105
|
+
cursor: 'pointer',
|
|
106
|
+
textAlign: 'left',
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<div style={{ fontWeight: 700 }}>{item.id}</div>
|
|
110
|
+
<div style={{ fontSize: '0.875rem', color: colorFor(status) }}>{status}</div>
|
|
111
|
+
{item.cycles_used !== undefined && (
|
|
112
|
+
<div style={{ fontSize: '0.75rem', color: '#9ca3af' }}>
|
|
113
|
+
{Number(item.cycles_used).toLocaleString()} cycles
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<Dialog
|
|
122
|
+
header={editing ? `Edit ${editing.id}` : 'Edit'}
|
|
123
|
+
visible={!!editing}
|
|
124
|
+
onHide={() => setEditing(null)}
|
|
125
|
+
style={{ width: '24rem' }}
|
|
126
|
+
footer={
|
|
127
|
+
<>
|
|
128
|
+
<Button label="Cancel" severity="secondary" onClick={() => setEditing(null)} />
|
|
129
|
+
<Button label="Save" icon="pi pi-check" onClick={onSave} />
|
|
130
|
+
</>
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
{editing && (
|
|
134
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '0.5rem 1rem' }}>
|
|
135
|
+
<label>Status</label>
|
|
136
|
+
<Dropdown
|
|
137
|
+
value={editing.status}
|
|
138
|
+
options={['available', 'in_use', 'worn', 'retired'].map(v => ({ label: v, value: v }))}
|
|
139
|
+
onChange={(e) => setEditing(s => s ? { ...s, status: e.value } : s)}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</Dialog>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public surface of the AMS component family. Drop <AmsProvider> at
|
|
3
|
+
* the top of your HMI; the rest are zero-prop and read from context.
|
|
4
|
+
*
|
|
5
|
+
* See autocore-server/doc/ams_product_plan.md for the full design.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { AmsProvider, useAms, useAmsSchemas, useAmsAlerts, useAmsAssets, useAmsSelection } from './AmsProvider';
|
|
9
|
+
export { AssetRegistryTable } from './AssetRegistryTable';
|
|
10
|
+
export { AssetDetailView } from './AssetDetailView';
|
|
11
|
+
export { CalibrationEntryDialog } from './CalibrationEntryDialog';
|
|
12
|
+
export { SubLocationPicker } from './SubLocationPicker';
|
package/src/components/index.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type {
|
|
|
27
27
|
TisMethodSchema,
|
|
28
28
|
} from './tis/TisProvider';
|
|
29
29
|
|
|
30
|
+
export { ProjectSelector } from './tis/ProjectSelector';
|
|
31
|
+
export type { ProjectSelectorProps } from './tis/ProjectSelector';
|
|
32
|
+
|
|
30
33
|
export { TestSetupForm } from './tis/TestSetupForm';
|
|
31
34
|
export type { TestSetupFormProps } from './tis/TestSetupForm';
|
|
32
35
|
|
|
@@ -44,3 +47,30 @@ export type { TestDataViewProps, ChartAxis, ChartSeries, ChartView, RawDataShape
|
|
|
44
47
|
|
|
45
48
|
export { TestRawDataView } from './tis/TestRawDataView';
|
|
46
49
|
export type { TestRawDataViewProps } from './tis/TestRawDataView';
|
|
50
|
+
|
|
51
|
+
// -----------------------------------------------------------------------
|
|
52
|
+
// Asset Management System — see autocore-server/doc/ams_product_plan.md
|
|
53
|
+
// Drop <AmsProvider> at the top of your HMI; the rest are zero-prop.
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
export {
|
|
56
|
+
AmsProvider,
|
|
57
|
+
useAms,
|
|
58
|
+
useAmsSchemas,
|
|
59
|
+
useAmsAlerts,
|
|
60
|
+
useAmsAssets,
|
|
61
|
+
useAmsSelection,
|
|
62
|
+
} from './ams/AmsProvider';
|
|
63
|
+
export type {
|
|
64
|
+
AmsProviderProps,
|
|
65
|
+
AmsContextValue,
|
|
66
|
+
AmsAlerts,
|
|
67
|
+
AmsAssetEntry,
|
|
68
|
+
AmsSelection,
|
|
69
|
+
AmsSchemaRegistry,
|
|
70
|
+
AmsTypeSchema,
|
|
71
|
+
} from './ams/AmsProvider';
|
|
72
|
+
export { AssetRegistryTable } from './ams/AssetRegistryTable';
|
|
73
|
+
export { AssetDetailView } from './ams/AssetDetailView';
|
|
74
|
+
export { CalibrationEntryDialog } from './ams/CalibrationEntryDialog';
|
|
75
|
+
export type { CalibrationEntryDialogProps } from './ams/CalibrationEntryDialog';
|
|
76
|
+
export { SubLocationPicker } from './ams/SubLocationPicker';
|