@happyvertical/smrt-manufacturing 0.53.6 → 0.54.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/AGENTS.md +1 -2
- package/README.md +3 -4
- package/dist/index.js +1 -1
- package/dist/manifest.json +1 -2
- package/dist/smrt-knowledge.json +5 -7
- package/dist/svelte/components/AssemblyForm.svelte +423 -52
- package/dist/svelte/components/AssemblyForm.svelte.d.ts +17 -32
- package/dist/svelte/components/AssemblyForm.svelte.d.ts.map +1 -1
- package/dist/svelte/components/AssemblyList.svelte +18 -12
- package/dist/svelte/components/AssemblyList.svelte.d.ts +2 -13
- package/dist/svelte/components/AssemblyList.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +23 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +23 -0
- package/dist/svelte/index.d.ts +4 -4
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -4
- package/dist/svelte/types.d.ts +95 -0
- package/dist/svelte/types.d.ts.map +1 -1
- package/dist/svelte/types.js +127 -0
- package/package.json +8 -9
|
@@ -1,69 +1,440 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* AssemblyForm —
|
|
4
|
-
*
|
|
3
|
+
* AssemblyForm — add an assembly, or edit one: name, description, category,
|
|
4
|
+
* part reference, price, estimated labour, default operation and tags.
|
|
5
|
+
* Presentational: it never persists. The host receives trimmed, checked
|
|
6
|
+
* values in `onsubmit` and saves them through the Assembly REST route or
|
|
7
|
+
* `AssemblyCollection`.
|
|
5
8
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* rendered. Pass `fields` and `policy` directly, or omit both beneath an
|
|
10
|
-
* `ObjectFormSourceProvider` that knows the Assembly definition.
|
|
9
|
+
* The host passes the operations to choose from (the active ones). An
|
|
10
|
+
* operation already set on the assembly that is retired, or no longer in the
|
|
11
|
+
* list, stays visible as the current value, marked, and can be cleared.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Field policy: a field named in `hiddenFields` is not rendered, and one in
|
|
14
|
+
* `readonlyFields` is shown but not editable (a price a viewer may see but
|
|
15
|
+
* not change). Neither is checked, and `onsubmit` carries its initial value
|
|
16
|
+
* unchanged, so a host should save only fields the reader may write.
|
|
15
17
|
*/
|
|
16
18
|
|
|
19
|
+
import { Button } from '@happyvertical/smrt-ui';
|
|
20
|
+
import { Form, Input, Select, Textarea } from '@happyvertical/smrt-ui/forms';
|
|
21
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
22
|
+
import { M } from '../i18n.js';
|
|
17
23
|
import {
|
|
18
|
-
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
type AssemblyFormField,
|
|
25
|
+
type AssemblyFormInitial,
|
|
26
|
+
type AssemblyFormInvalidField,
|
|
27
|
+
type AssemblyFormValues,
|
|
28
|
+
assemblyFormDraft,
|
|
29
|
+
currencyExponent,
|
|
30
|
+
keepProtectedFields,
|
|
31
|
+
type OperationView,
|
|
32
|
+
validateAssemblyForm,
|
|
33
|
+
} from '../types.js';
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
const ASSEMBLY_OBJECT_REF = '@happyvertical/smrt-manufacturing:Assembly';
|
|
35
|
+
const { t } = useI18n();
|
|
26
36
|
|
|
27
37
|
export interface AssemblyFormProps {
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
/** True for a new assembly (policy defaults may prefill), false for an edit. */
|
|
35
|
-
isNewRecord?: boolean;
|
|
38
|
+
/** The assembly being edited; omit or pass null to add one. */
|
|
39
|
+
assembly?: AssemblyFormInitial | null;
|
|
40
|
+
/** Invoked with trimmed, checked values. The form never persists. */
|
|
41
|
+
onsubmit: (values: AssemblyFormValues) => void;
|
|
42
|
+
/** Invoked when the form is canceled; omit to hide the cancel button. */
|
|
43
|
+
oncancel?: () => void;
|
|
36
44
|
/** Blocks input and submission while the host saves. */
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
|
|
45
|
+
loading?: boolean;
|
|
46
|
+
/** Operations to choose a default from: the active ones, host-loaded. */
|
|
47
|
+
operations?: readonly OperationView[];
|
|
48
|
+
/** ISO 4217 currency of the price, named in its help text. */
|
|
49
|
+
currency?: string;
|
|
50
|
+
/** Fields the reader may not see: not rendered. */
|
|
51
|
+
hiddenFields?: readonly AssemblyFormField[];
|
|
52
|
+
/** Fields the reader may see but not change: rendered read-only. */
|
|
53
|
+
readonlyFields?: readonly AssemblyFormField[];
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
policy,
|
|
49
|
-
value = $bindable<Record<string, unknown>>({}),
|
|
50
|
-
isNewRecord = true,
|
|
51
|
-
disabled = false,
|
|
52
|
-
showModeSwitch = true,
|
|
53
|
-
actions,
|
|
56
|
+
const {
|
|
57
|
+
assembly = null,
|
|
54
58
|
onsubmit,
|
|
59
|
+
oncancel,
|
|
60
|
+
loading = false,
|
|
61
|
+
operations = [],
|
|
62
|
+
currency = 'USD',
|
|
63
|
+
hiddenFields = [],
|
|
64
|
+
readonlyFields = [],
|
|
55
65
|
}: AssemblyFormProps = $props();
|
|
66
|
+
|
|
67
|
+
const uid = $props.id();
|
|
68
|
+
const exponent = $derived(currencyExponent(currency));
|
|
69
|
+
const isNew = $derived(assembly === null);
|
|
70
|
+
|
|
71
|
+
function getInitialFormState() {
|
|
72
|
+
const initialExponent = currencyExponent(currency);
|
|
73
|
+
return {
|
|
74
|
+
assembly,
|
|
75
|
+
exponent: initialExponent,
|
|
76
|
+
draft: assemblyFormDraft(assembly, initialExponent),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const initialFormState = getInitialFormState();
|
|
81
|
+
let name = $state(initialFormState.draft.name);
|
|
82
|
+
let description = $state(initialFormState.draft.description);
|
|
83
|
+
let category = $state(initialFormState.draft.category);
|
|
84
|
+
let partReference = $state(initialFormState.draft.partReference);
|
|
85
|
+
let price = $state(initialFormState.draft.price);
|
|
86
|
+
let estimatedLabourMinutes = $state(
|
|
87
|
+
initialFormState.draft.estimatedLabourMinutes,
|
|
88
|
+
);
|
|
89
|
+
let defaultOperationId = $state(initialFormState.draft.defaultOperationId);
|
|
90
|
+
let tags = $state(initialFormState.draft.tags);
|
|
91
|
+
let invalid = $state<AssemblyFormInvalidField[]>([]);
|
|
92
|
+
let appliedAssembly: AssemblyFormInitial | null = initialFormState.assembly;
|
|
93
|
+
let appliedExponent = initialFormState.exponent;
|
|
94
|
+
|
|
95
|
+
// Follow the host when it swaps the assembly being edited.
|
|
96
|
+
// Follow a change of currency: show the stored price in the new minor unit.
|
|
97
|
+
$effect(() => {
|
|
98
|
+
if (appliedExponent === exponent) return;
|
|
99
|
+
appliedExponent = exponent;
|
|
100
|
+
price = assemblyFormDraft(assembly, exponent).price;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (appliedAssembly === assembly) return;
|
|
105
|
+
appliedAssembly = assembly;
|
|
106
|
+
const draft = assemblyFormDraft(assembly, exponent);
|
|
107
|
+
name = draft.name;
|
|
108
|
+
description = draft.description;
|
|
109
|
+
category = draft.category;
|
|
110
|
+
partReference = draft.partReference;
|
|
111
|
+
price = draft.price;
|
|
112
|
+
estimatedLabourMinutes = draft.estimatedLabourMinutes;
|
|
113
|
+
defaultOperationId = draft.defaultOperationId;
|
|
114
|
+
tags = draft.tags;
|
|
115
|
+
invalid = [];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const shown = (field: AssemblyFormField) => !hiddenFields.includes(field);
|
|
119
|
+
const locked = (field: AssemblyFormField) =>
|
|
120
|
+
loading || readonlyFields.includes(field);
|
|
121
|
+
|
|
122
|
+
// The operation set when the form opened, kept in the list even if retired.
|
|
123
|
+
const originalOperationId = $derived(assembly?.defaultOperationId ?? '');
|
|
124
|
+
|
|
125
|
+
function operationLabel(operation: OperationView): string {
|
|
126
|
+
const label = operation.name
|
|
127
|
+
? `${operation.code} - ${operation.name}`
|
|
128
|
+
: operation.code;
|
|
129
|
+
return operation.isActive
|
|
130
|
+
? label
|
|
131
|
+
: t(M['manufacturing.assembly_form.operation_retired'], { label });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const operationChoices = $derived.by(() => {
|
|
135
|
+
const choices: { id: string; label: string }[] = operations
|
|
136
|
+
.filter(
|
|
137
|
+
(operation) => operation.isActive || operation.id === originalOperationId,
|
|
138
|
+
)
|
|
139
|
+
.map((operation) => ({
|
|
140
|
+
id: operation.id,
|
|
141
|
+
label: operationLabel(operation),
|
|
142
|
+
}));
|
|
143
|
+
if (
|
|
144
|
+
originalOperationId &&
|
|
145
|
+
!choices.some((c) => c.id === originalOperationId)
|
|
146
|
+
) {
|
|
147
|
+
choices.push({
|
|
148
|
+
id: originalOperationId,
|
|
149
|
+
label: t(M['manufacturing.assembly_form.operation_unknown'], {
|
|
150
|
+
id: originalOperationId,
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return choices;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
function errorMessage(field: AssemblyFormInvalidField): string {
|
|
158
|
+
switch (field) {
|
|
159
|
+
case 'name':
|
|
160
|
+
return t(M['manufacturing.assembly_form.error_name']);
|
|
161
|
+
case 'price':
|
|
162
|
+
return t(M['manufacturing.assembly_form.error_price']);
|
|
163
|
+
default:
|
|
164
|
+
return t(M['manufacturing.assembly_form.error_labour']);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const errors = $derived(
|
|
169
|
+
invalid.map((field) => ({ field, message: errorMessage(field) })),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
function errorId(field: AssemblyFormInvalidField): string {
|
|
173
|
+
return `${uid}-error-${field}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function describedBy(
|
|
177
|
+
field: AssemblyFormInvalidField | null,
|
|
178
|
+
hintId?: string,
|
|
179
|
+
): string | undefined {
|
|
180
|
+
const ids = [
|
|
181
|
+
hintId,
|
|
182
|
+
field && invalid.includes(field) ? errorId(field) : undefined,
|
|
183
|
+
];
|
|
184
|
+
return ids.filter(Boolean).join(' ') || undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const policyProtected = $derived([...hiddenFields, ...readonlyFields]);
|
|
188
|
+
|
|
189
|
+
function handleSubmit() {
|
|
190
|
+
const draft = {
|
|
191
|
+
name,
|
|
192
|
+
description,
|
|
193
|
+
category,
|
|
194
|
+
partReference,
|
|
195
|
+
price,
|
|
196
|
+
estimatedLabourMinutes,
|
|
197
|
+
defaultOperationId,
|
|
198
|
+
tags,
|
|
199
|
+
};
|
|
200
|
+
// A field the reader left as it was keeps its stored value exactly (a tag
|
|
201
|
+
// with a comma, a price this form cannot express); the name is always checked.
|
|
202
|
+
const initialDraft = assemblyFormDraft(assembly, exponent);
|
|
203
|
+
const untouched = (Object.keys(draft) as AssemblyFormField[]).filter(
|
|
204
|
+
(field) => field !== 'name' && draft[field] === initialDraft[field],
|
|
205
|
+
);
|
|
206
|
+
const protectedFields = [...policyProtected, ...untouched];
|
|
207
|
+
const result = validateAssemblyForm(draft, policyProtected, exponent);
|
|
208
|
+
const checked =
|
|
209
|
+
result.ok || untouched.length === 0
|
|
210
|
+
? result
|
|
211
|
+
: validateAssemblyForm(draft, protectedFields, exponent);
|
|
212
|
+
if (!checked.ok) {
|
|
213
|
+
invalid = checked.invalid;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
invalid = [];
|
|
217
|
+
onsubmit(keepProtectedFields(checked.values, assembly, protectedFields));
|
|
218
|
+
}
|
|
56
219
|
</script>
|
|
57
220
|
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
221
|
+
<div class="assembly-form-shell">
|
|
222
|
+
<Form class="assembly-form" onsubmit={handleSubmit}>
|
|
223
|
+
{#if shown('name')}
|
|
224
|
+
<div class="field">
|
|
225
|
+
<label for="{uid}-name">
|
|
226
|
+
{t(M['manufacturing.assembly_form.name'])}<span class="required" aria-hidden="true">*</span>
|
|
227
|
+
</label>
|
|
228
|
+
<Input
|
|
229
|
+
id="{uid}-name"
|
|
230
|
+
name="name"
|
|
231
|
+
type="text"
|
|
232
|
+
bind:value={name}
|
|
233
|
+
required
|
|
234
|
+
readonly={readonlyFields.includes('name')}
|
|
235
|
+
disabled={loading}
|
|
236
|
+
aria-invalid={invalid.includes('name') ? 'true' : undefined}
|
|
237
|
+
aria-describedby={describedBy('name')}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
{/if}
|
|
241
|
+
|
|
242
|
+
{#if shown('description')}
|
|
243
|
+
<div class="field">
|
|
244
|
+
<label for="{uid}-description">{t(M['manufacturing.assembly_form.description'])}</label>
|
|
245
|
+
<Textarea
|
|
246
|
+
id="{uid}-description"
|
|
247
|
+
name="description"
|
|
248
|
+
bind:value={description}
|
|
249
|
+
readonly={readonlyFields.includes('description')}
|
|
250
|
+
disabled={loading}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
{/if}
|
|
254
|
+
|
|
255
|
+
{#if shown('partReference')}
|
|
256
|
+
<div class="field">
|
|
257
|
+
<label for="{uid}-part-reference">{t(M['manufacturing.assembly_form.part_reference'])}</label>
|
|
258
|
+
<Input
|
|
259
|
+
id="{uid}-part-reference"
|
|
260
|
+
name="partReference"
|
|
261
|
+
type="text"
|
|
262
|
+
bind:value={partReference}
|
|
263
|
+
readonly={readonlyFields.includes('partReference')}
|
|
264
|
+
disabled={loading}
|
|
265
|
+
aria-describedby="{uid}-part-reference-hint"
|
|
266
|
+
/>
|
|
267
|
+
<span class="hint" id="{uid}-part-reference-hint">
|
|
268
|
+
{t(M['manufacturing.assembly_form.part_reference_help'])}
|
|
269
|
+
</span>
|
|
270
|
+
</div>
|
|
271
|
+
{/if}
|
|
272
|
+
|
|
273
|
+
{#if shown('category')}
|
|
274
|
+
<div class="field">
|
|
275
|
+
<label for="{uid}-category">{t(M['manufacturing.assembly_form.category'])}</label>
|
|
276
|
+
<Input
|
|
277
|
+
id="{uid}-category"
|
|
278
|
+
name="category"
|
|
279
|
+
type="text"
|
|
280
|
+
bind:value={category}
|
|
281
|
+
readonly={readonlyFields.includes('category')}
|
|
282
|
+
disabled={loading}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
{/if}
|
|
286
|
+
|
|
287
|
+
{#if shown('price')}
|
|
288
|
+
<div class="field">
|
|
289
|
+
<label for="{uid}-price">{t(M['manufacturing.assembly_form.price'])}</label>
|
|
290
|
+
<Input
|
|
291
|
+
id="{uid}-price"
|
|
292
|
+
name="price"
|
|
293
|
+
type="text"
|
|
294
|
+
inputmode="decimal"
|
|
295
|
+
autocomplete="off"
|
|
296
|
+
bind:value={price}
|
|
297
|
+
readonly={readonlyFields.includes('price')}
|
|
298
|
+
disabled={loading}
|
|
299
|
+
aria-invalid={invalid.includes('price') ? 'true' : undefined}
|
|
300
|
+
aria-describedby={describedBy('price', `${uid}-price-hint`)}
|
|
301
|
+
/>
|
|
302
|
+
<span class="hint" id="{uid}-price-hint">
|
|
303
|
+
{t(M['manufacturing.assembly_form.price_help'], { currency })}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
{/if}
|
|
307
|
+
|
|
308
|
+
{#if shown('estimatedLabourMinutes')}
|
|
309
|
+
<div class="field">
|
|
310
|
+
<label for="{uid}-labour">{t(M['manufacturing.assembly_form.labour'])}</label>
|
|
311
|
+
<Input
|
|
312
|
+
id="{uid}-labour"
|
|
313
|
+
name="estimatedLabourMinutes"
|
|
314
|
+
type="text"
|
|
315
|
+
inputmode="numeric"
|
|
316
|
+
autocomplete="off"
|
|
317
|
+
bind:value={estimatedLabourMinutes}
|
|
318
|
+
readonly={readonlyFields.includes('estimatedLabourMinutes')}
|
|
319
|
+
disabled={loading}
|
|
320
|
+
aria-invalid={invalid.includes('estimatedLabourMinutes') ? 'true' : undefined}
|
|
321
|
+
aria-describedby={describedBy('estimatedLabourMinutes', `${uid}-labour-hint`)}
|
|
322
|
+
/>
|
|
323
|
+
<span class="hint" id="{uid}-labour-hint">
|
|
324
|
+
{t(M['manufacturing.assembly_form.labour_help'])}
|
|
325
|
+
</span>
|
|
326
|
+
</div>
|
|
327
|
+
{/if}
|
|
328
|
+
|
|
329
|
+
{#if shown('defaultOperationId')}
|
|
330
|
+
<div class="field">
|
|
331
|
+
<label for="{uid}-operation">{t(M['manufacturing.assembly_form.operation'])}</label>
|
|
332
|
+
<Select
|
|
333
|
+
id="{uid}-operation"
|
|
334
|
+
name="defaultOperationId"
|
|
335
|
+
bind:value={defaultOperationId}
|
|
336
|
+
disabled={locked('defaultOperationId')}
|
|
337
|
+
aria-describedby="{uid}-operation-hint"
|
|
338
|
+
>
|
|
339
|
+
<option value="">{t(M['manufacturing.assembly_form.operation_none'])}</option>
|
|
340
|
+
{#each operationChoices as choice (choice.id)}
|
|
341
|
+
<option value={choice.id}>{choice.label}</option>
|
|
342
|
+
{/each}
|
|
343
|
+
</Select>
|
|
344
|
+
<span class="hint" id="{uid}-operation-hint">
|
|
345
|
+
{t(M['manufacturing.assembly_form.operation_help'])}
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
{/if}
|
|
349
|
+
|
|
350
|
+
{#if shown('tags')}
|
|
351
|
+
<div class="field">
|
|
352
|
+
<label for="{uid}-tags">{t(M['manufacturing.assembly_form.tags'])}</label>
|
|
353
|
+
<Input
|
|
354
|
+
id="{uid}-tags"
|
|
355
|
+
name="tags"
|
|
356
|
+
type="text"
|
|
357
|
+
autocomplete="off"
|
|
358
|
+
bind:value={tags}
|
|
359
|
+
readonly={readonlyFields.includes('tags')}
|
|
360
|
+
disabled={loading}
|
|
361
|
+
aria-describedby="{uid}-tags-hint"
|
|
362
|
+
/>
|
|
363
|
+
<span class="hint" id="{uid}-tags-hint">
|
|
364
|
+
{t(M['manufacturing.assembly_form.tags_help'])}
|
|
365
|
+
</span>
|
|
366
|
+
</div>
|
|
367
|
+
{/if}
|
|
368
|
+
|
|
369
|
+
{#if errors.length > 0}
|
|
370
|
+
<div class="errors" role="alert">
|
|
371
|
+
<ul>
|
|
372
|
+
{#each errors as error (error.field)}
|
|
373
|
+
<li id={errorId(error.field)}>{error.message}</li>
|
|
374
|
+
{/each}
|
|
375
|
+
</ul>
|
|
376
|
+
</div>
|
|
377
|
+
{/if}
|
|
378
|
+
|
|
379
|
+
<div class="actions">
|
|
380
|
+
{#if oncancel}
|
|
381
|
+
<Button variant="secondary" type="button" onclick={oncancel} disabled={loading}>
|
|
382
|
+
{t(M['manufacturing.assembly_form.cancel'])}
|
|
383
|
+
</Button>
|
|
384
|
+
{/if}
|
|
385
|
+
<Button variant="primary" type="submit" disabled={loading}>
|
|
386
|
+
{#if loading}
|
|
387
|
+
{t(M['manufacturing.assembly_form.saving'])}
|
|
388
|
+
{:else if isNew}
|
|
389
|
+
{t(M['manufacturing.assembly_form.submit_add'])}
|
|
390
|
+
{:else}
|
|
391
|
+
{t(M['manufacturing.assembly_form.submit_edit'])}
|
|
392
|
+
{/if}
|
|
393
|
+
</Button>
|
|
394
|
+
</div>
|
|
395
|
+
</Form>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<style>
|
|
399
|
+
.assembly-form-shell :global(.assembly-form) {
|
|
400
|
+
display: flex;
|
|
401
|
+
flex-direction: column;
|
|
402
|
+
gap: var(--smrt-spacing-md, 1rem);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.field {
|
|
406
|
+
display: flex;
|
|
407
|
+
flex-direction: column;
|
|
408
|
+
gap: 0.25rem;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
label {
|
|
412
|
+
font: var(--smrt-typography-body-medium-font, 500 0.875rem / 1.25 sans-serif);
|
|
413
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.required {
|
|
417
|
+
color: var(--smrt-color-error, inherit);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.hint {
|
|
421
|
+
font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
|
|
422
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.errors {
|
|
426
|
+
color: var(--smrt-color-error, inherit);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.errors ul {
|
|
430
|
+
margin: 0;
|
|
431
|
+
padding-left: 1.25rem;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.actions {
|
|
435
|
+
display: flex;
|
|
436
|
+
justify-content: flex-end;
|
|
437
|
+
gap: var(--smrt-spacing-sm, 0.5rem);
|
|
438
|
+
margin-top: var(--smrt-spacing-sm, 0.5rem);
|
|
439
|
+
}
|
|
440
|
+
</style>
|
|
@@ -1,38 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
* AssemblyForm — the create and edit form for an assembly, rendered by
|
|
3
|
-
* smrt-fields `ObjectForm` for `@happyvertical/smrt-manufacturing:Assembly`.
|
|
4
|
-
*
|
|
5
|
-
* The fields, their order, labels, help and visibility come from the
|
|
6
|
-
* generated Assembly definition and the consumer's resolved field policy, so
|
|
7
|
-
* a field the policy hides (price, for someone who may not see it) is not
|
|
8
|
-
* rendered. Pass `fields` and `policy` directly, or omit both beneath an
|
|
9
|
-
* `ObjectFormSourceProvider` that knows the Assembly definition.
|
|
10
|
-
*
|
|
11
|
-
* Presentational: the host owns the record and persistence. `onsubmit`
|
|
12
|
-
* should save through the Assembly REST route or `AssemblyCollection` and
|
|
13
|
-
* resolve `true` once the record is stored.
|
|
14
|
-
*/
|
|
15
|
-
import { type ObjectFormProps, type ResolvedObjectFieldPolicy } from '@happyvertical/smrt-fields/svelte';
|
|
16
|
-
import type { Snippet } from 'svelte';
|
|
1
|
+
import { type AssemblyFormField, type AssemblyFormInitial, type AssemblyFormValues, type OperationView } from '../types.js';
|
|
17
2
|
export interface AssemblyFormProps {
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
/** True for a new assembly (policy defaults may prefill), false for an edit. */
|
|
25
|
-
isNewRecord?: boolean;
|
|
3
|
+
/** The assembly being edited; omit or pass null to add one. */
|
|
4
|
+
assembly?: AssemblyFormInitial | null;
|
|
5
|
+
/** Invoked with trimmed, checked values. The form never persists. */
|
|
6
|
+
onsubmit: (values: AssemblyFormValues) => void;
|
|
7
|
+
/** Invoked when the form is canceled; omit to hide the cancel button. */
|
|
8
|
+
oncancel?: () => void;
|
|
26
9
|
/** Blocks input and submission while the host saves. */
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
/** Operations to choose a default from: the active ones, host-loaded. */
|
|
12
|
+
operations?: readonly OperationView[];
|
|
13
|
+
/** ISO 4217 currency of the price, named in its help text. */
|
|
14
|
+
currency?: string;
|
|
15
|
+
/** Fields the reader may not see: not rendered. */
|
|
16
|
+
hiddenFields?: readonly AssemblyFormField[];
|
|
17
|
+
/** Fields the reader may see but not change: rendered read-only. */
|
|
18
|
+
readonlyFields?: readonly AssemblyFormField[];
|
|
34
19
|
}
|
|
35
|
-
declare const AssemblyForm: import("svelte").Component<AssemblyFormProps, {}, "
|
|
20
|
+
declare const AssemblyForm: import("svelte").Component<AssemblyFormProps, {}, "">;
|
|
36
21
|
type AssemblyForm = ReturnType<typeof AssemblyForm>;
|
|
37
22
|
export default AssemblyForm;
|
|
38
23
|
//# sourceMappingURL=AssemblyForm.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssemblyForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyForm.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AssemblyForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyForm.svelte.ts"],"names":[],"mappings":"AAuBA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAExB,KAAK,kBAAkB,EAIvB,KAAK,aAAa,EAEnB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACtC,qEAAqE;IACrE,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,UAAU,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,YAAY,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,oEAAoE;IACpE,cAAc,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C;AAmSD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
* the host loads assemblies, their SKUs and active bills, and adapts each row
|
|
6
6
|
* with `toAssemblyView`.
|
|
7
7
|
*
|
|
8
|
-
* Pass the resolved field `policy` for
|
|
9
|
-
* to apply the consumer's field policy: a field the policy hides (price, for
|
|
8
|
+
* Pass the resolved field `policy` for Assembly (any object whose `fields`
|
|
9
|
+
* name each field's `visibility`) to apply the consumer's field policy: a field the policy hides (price, for
|
|
10
10
|
* a viewer who may not see it) drops its column.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
policyToVisibleColumnIds,
|
|
15
|
-
type ResolvedObjectFieldPolicy,
|
|
16
|
-
} from '@happyvertical/smrt-fields/svelte';
|
|
17
13
|
import { Button, CurrencyDisplay } from '@happyvertical/smrt-ui';
|
|
18
14
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
19
15
|
import { M } from '../i18n.js';
|
|
20
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AssemblyFieldPolicy,
|
|
18
|
+
type AssemblyView,
|
|
19
|
+
splitLabourMinutes,
|
|
20
|
+
} from '../types.js';
|
|
21
21
|
|
|
22
22
|
const { t } = useI18n();
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ export interface AssemblyListProps {
|
|
|
25
25
|
/** Assemblies to show, in the host's order. */
|
|
26
26
|
assemblies: AssemblyView[];
|
|
27
27
|
/** The resolved field policy for Assembly; hidden fields drop their column. */
|
|
28
|
-
policy?:
|
|
28
|
+
policy?: AssemblyFieldPolicy;
|
|
29
29
|
/** ISO 4217 currency of the prices. */
|
|
30
30
|
currency?: string;
|
|
31
31
|
/** Invoked with the assembly id when a name is activated. */
|
|
@@ -53,12 +53,18 @@ const POLICY_COLUMNS = [
|
|
|
53
53
|
{ id: 'labour' },
|
|
54
54
|
{ id: 'bill' },
|
|
55
55
|
];
|
|
56
|
-
const FIELD_BY_COLUMN = {
|
|
56
|
+
const FIELD_BY_COLUMN: Record<string, string> = {
|
|
57
|
+
labour: 'estimatedLabourMinutes',
|
|
58
|
+
};
|
|
57
59
|
|
|
58
60
|
const visible = $derived(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
new Set(
|
|
62
|
+
POLICY_COLUMNS.filter(
|
|
63
|
+
(column) =>
|
|
64
|
+
policy?.fields[FIELD_BY_COLUMN[column.id] ?? column.id]?.visibility !==
|
|
65
|
+
'hidden',
|
|
66
|
+
).map((column) => column.id),
|
|
67
|
+
),
|
|
62
68
|
);
|
|
63
69
|
|
|
64
70
|
/** The row header: the name, or a visible identifier when the name is hidden. */
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* AssemblyList — a table of assemblies: name, part reference, SKU codes,
|
|
3
|
-
* price, labour estimate and the version of the active bill. Presentational:
|
|
4
|
-
* the host loads assemblies, their SKUs and active bills, and adapts each row
|
|
5
|
-
* with `toAssemblyView`.
|
|
6
|
-
*
|
|
7
|
-
* Pass the resolved field `policy` for `@happyvertical/smrt-manufacturing:Assembly`
|
|
8
|
-
* to apply the consumer's field policy: a field the policy hides (price, for
|
|
9
|
-
* a viewer who may not see it) drops its column.
|
|
10
|
-
*/
|
|
11
|
-
import { type ResolvedObjectFieldPolicy } from '@happyvertical/smrt-fields/svelte';
|
|
12
|
-
import { type AssemblyView } from '../types.js';
|
|
1
|
+
import { type AssemblyFieldPolicy, type AssemblyView } from '../types.js';
|
|
13
2
|
export interface AssemblyListProps {
|
|
14
3
|
/** Assemblies to show, in the host's order. */
|
|
15
4
|
assemblies: AssemblyView[];
|
|
16
5
|
/** The resolved field policy for Assembly; hidden fields drop their column. */
|
|
17
|
-
policy?:
|
|
6
|
+
policy?: AssemblyFieldPolicy;
|
|
18
7
|
/** ISO 4217 currency of the prices. */
|
|
19
8
|
currency?: string;
|
|
20
9
|
/** Invoked with the assembly id when a name is activated. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssemblyList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyList.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AssemblyList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AssemblyList.svelte.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA4JD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|