@happyvertical/smrt-manufacturing 0.53.3 → 0.53.5
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 +45 -6
- package/README.md +161 -3
- package/dist/collections/AssemblyCollection.d.ts +15 -0
- package/dist/collections/AssemblyCollection.d.ts.map +1 -0
- package/dist/collections/OperationCollection.d.ts +20 -0
- package/dist/collections/OperationCollection.d.ts.map +1 -0
- package/dist/collections/RoutingStepCollection.d.ts +15 -0
- package/dist/collections/RoutingStepCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +3 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +756 -29
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1248 -128
- package/dist/models/Assembly.d.ts +62 -0
- package/dist/models/Assembly.d.ts.map +1 -0
- package/dist/models/BillOfMaterials.d.ts +12 -4
- package/dist/models/BillOfMaterials.d.ts.map +1 -1
- package/dist/models/BomLine.d.ts +11 -5
- package/dist/models/BomLine.d.ts.map +1 -1
- package/dist/models/Operation.d.ts +59 -0
- package/dist/models/Operation.d.ts.map +1 -0
- package/dist/models/RoutingStep.d.ts +46 -0
- package/dist/models/RoutingStep.d.ts.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/operation-types.d.ts +99 -0
- package/dist/operation-types.d.ts.map +1 -0
- package/dist/services/AssemblyService.d.ts +217 -0
- package/dist/services/AssemblyService.d.ts.map +1 -0
- package/dist/services/BomService.d.ts +25 -1
- package/dist/services/BomService.d.ts.map +1 -1
- package/dist/services/OperationService.d.ts +48 -0
- package/dist/services/OperationService.d.ts.map +1 -0
- package/dist/services/RoutingService.d.ts +50 -0
- package/dist/services/RoutingService.d.ts.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +1138 -81
- package/dist/svelte/components/AssemblyForm.svelte +69 -0
- package/dist/svelte/components/AssemblyForm.svelte.d.ts +38 -0
- package/dist/svelte/components/AssemblyForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/AssemblyList.svelte +229 -0
- package/dist/svelte/components/AssemblyList.svelte.d.ts +28 -0
- package/dist/svelte/components/AssemblyList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/BomEditor.svelte +575 -0
- package/dist/svelte/components/BomEditor.svelte.d.ts +29 -0
- package/dist/svelte/components/BomEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/BomStructureTree.svelte +164 -0
- package/dist/svelte/components/BomStructureTree.svelte.d.ts +16 -0
- package/dist/svelte/components/BomStructureTree.svelte.d.ts.map +1 -0
- package/dist/svelte/components/OperationForm.svelte +258 -0
- package/dist/svelte/components/OperationForm.svelte.d.ts +17 -0
- package/dist/svelte/components/OperationForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/OperationList.svelte +170 -0
- package/dist/svelte/components/OperationList.svelte.d.ts +17 -0
- package/dist/svelte/components/OperationList.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +87 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +87 -0
- package/dist/svelte/index.d.ts +28 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +29 -0
- package/dist/svelte/types.d.ts +170 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +126 -0
- package/dist/ui.d.ts +17 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +72 -0
- package/dist/ui.js.map +1 -0
- package/package.json +39 -10
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BomEditor — edits the lines of one assembly's bill of materials: materials,
|
|
4
|
+
* bought items and sub-assemblies. Each sub-assembly says whether it has its
|
|
5
|
+
* own active bill and, with `loadBill`, expands to show that bill read-only,
|
|
6
|
+
* to any depth. A sub-assembly's own bill is edited on that assembly.
|
|
7
|
+
*
|
|
8
|
+
* Presentational: the host loads the bill with
|
|
9
|
+
* `AssemblyService.getBillStructure` and adapts it with `toBomEditorLines`,
|
|
10
|
+
* and saves through `BomLine`. The editor hands the host checked values. A
|
|
11
|
+
* save the package refuses (for example a `BomCycleError`, a line that would
|
|
12
|
+
* make the assembly contain itself) comes back to the host, which shows its
|
|
13
|
+
* message through `error`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Alert, Badge, Button } from '@happyvertical/smrt-ui';
|
|
17
|
+
import { Form, Input, Select } from '@happyvertical/smrt-ui/forms';
|
|
18
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
19
|
+
import { M } from '../i18n.js';
|
|
20
|
+
import {
|
|
21
|
+
type BomComponentOption,
|
|
22
|
+
type BomEditorLine,
|
|
23
|
+
type BomLineDraft,
|
|
24
|
+
type BomLineDraftField,
|
|
25
|
+
componentKindLabelKey,
|
|
26
|
+
validateBomLineInput,
|
|
27
|
+
} from '../types.js';
|
|
28
|
+
import BomStructureTree from './BomStructureTree.svelte';
|
|
29
|
+
|
|
30
|
+
const { t } = useI18n();
|
|
31
|
+
|
|
32
|
+
/** What a host handler returns: `false` keeps the editor's draft. */
|
|
33
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: a handler may return nothing.
|
|
34
|
+
type HandlerResult = boolean | void | Promise<boolean | void>;
|
|
35
|
+
|
|
36
|
+
export interface BomEditorProps {
|
|
37
|
+
/** The bill's lines, in the host's order. */
|
|
38
|
+
lines: BomEditorLine[];
|
|
39
|
+
/** Components offered when adding a line; omit to hide the add form. */
|
|
40
|
+
components?: BomComponentOption[];
|
|
41
|
+
/** Adds a line. Return `false` (or reject) to keep the draft. */
|
|
42
|
+
onadd?: (draft: BomLineDraft) => HandlerResult;
|
|
43
|
+
/** Saves an edited line's quantity, unit and waste. Omit for read-only rows. */
|
|
44
|
+
onupdate?: (id: string, draft: BomLineDraft) => HandlerResult;
|
|
45
|
+
/** Removes a line. Omit to hide the remove buttons. */
|
|
46
|
+
onremove?: (id: string) => HandlerResult;
|
|
47
|
+
/** Loads a sub-assembly's bill lines by bill id, for the read-only expansion. */
|
|
48
|
+
loadBill?: (bomId: string) => Promise<BomEditorLine[]>;
|
|
49
|
+
/** The id of the bill being edited; its own bill is never expanded inside it. */
|
|
50
|
+
bomId?: string;
|
|
51
|
+
/** A message from the host, such as a refused save. */
|
|
52
|
+
error?: string | null;
|
|
53
|
+
/** Blocks every control while the host saves. */
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** Unit prefilled for a new line. */
|
|
56
|
+
defaultUom?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
lines,
|
|
61
|
+
components,
|
|
62
|
+
onadd,
|
|
63
|
+
onupdate,
|
|
64
|
+
onremove,
|
|
65
|
+
loadBill,
|
|
66
|
+
bomId,
|
|
67
|
+
error = null,
|
|
68
|
+
disabled = false,
|
|
69
|
+
defaultUom = 'each',
|
|
70
|
+
}: BomEditorProps = $props();
|
|
71
|
+
|
|
72
|
+
const uid = $props.id();
|
|
73
|
+
|
|
74
|
+
type RowDraft = { qtyPerUnit: string; uom: string; wastePercent: string };
|
|
75
|
+
|
|
76
|
+
let rowDrafts = $state<Record<string, RowDraft>>({});
|
|
77
|
+
let rowInvalid = $state<Record<string, BomLineDraftField[]>>({});
|
|
78
|
+
let open = $state<Record<string, boolean>>({});
|
|
79
|
+
let loadedBills = $state<
|
|
80
|
+
Record<
|
|
81
|
+
string,
|
|
82
|
+
| { state: 'loading' }
|
|
83
|
+
| { state: 'ready'; lines: BomEditorLine[] }
|
|
84
|
+
| { state: 'failed' }
|
|
85
|
+
>
|
|
86
|
+
>({});
|
|
87
|
+
|
|
88
|
+
let addComponent = $state('');
|
|
89
|
+
let addQty = $state('1');
|
|
90
|
+
// The prefilled unit is read once; `add()` resets to the current prop.
|
|
91
|
+
function initialUom(): string {
|
|
92
|
+
return defaultUom;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let addUom = $state(initialUom());
|
|
96
|
+
let addWaste = $state('0');
|
|
97
|
+
let addInvalid = $state<BomLineDraftField[]>([]);
|
|
98
|
+
let busy = $state(false);
|
|
99
|
+
|
|
100
|
+
const blocked = $derived(disabled || busy);
|
|
101
|
+
|
|
102
|
+
function draftOf(line: BomEditorLine): RowDraft {
|
|
103
|
+
return (
|
|
104
|
+
rowDrafts[line.id] ?? {
|
|
105
|
+
qtyPerUnit: String(line.qtyPerUnit),
|
|
106
|
+
uom: line.uom,
|
|
107
|
+
wastePercent: String(line.wastePercent),
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function edit(line: BomEditorLine, field: keyof RowDraft, value: string) {
|
|
113
|
+
rowDrafts[line.id] = { ...draftOf(line), [field]: value };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isDirty(line: BomEditorLine): boolean {
|
|
117
|
+
const draft = rowDrafts[line.id];
|
|
118
|
+
if (!draft) return false;
|
|
119
|
+
return (
|
|
120
|
+
draft.qtyPerUnit !== String(line.qtyPerUnit) ||
|
|
121
|
+
draft.uom !== line.uom ||
|
|
122
|
+
draft.wastePercent !== String(line.wastePercent)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function componentText(line: BomEditorLine): string {
|
|
127
|
+
return line.skuCode && line.skuCode !== line.componentName
|
|
128
|
+
? `${line.componentName} (${line.skuCode})`
|
|
129
|
+
: line.componentName;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function run(handler: () => HandlerResult): Promise<boolean> {
|
|
133
|
+
busy = true;
|
|
134
|
+
try {
|
|
135
|
+
return (await handler()) !== false;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
} finally {
|
|
139
|
+
busy = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function saveRow(line: BomEditorLine) {
|
|
144
|
+
if (!onupdate) return;
|
|
145
|
+
const draft = draftOf(line);
|
|
146
|
+
const result = validateBomLineInput({
|
|
147
|
+
componentSkuId: line.componentSkuId,
|
|
148
|
+
...draft,
|
|
149
|
+
});
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
rowInvalid[line.id] = result.invalid;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
rowInvalid[line.id] = [];
|
|
155
|
+
if (await run(() => onupdate(line.id, result.value))) {
|
|
156
|
+
delete rowDrafts[line.id];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function removeRow(line: BomEditorLine) {
|
|
161
|
+
if (!onremove) return;
|
|
162
|
+
if (await run(() => onremove(line.id))) {
|
|
163
|
+
delete rowDrafts[line.id];
|
|
164
|
+
delete rowInvalid[line.id];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function toggle(line: BomEditorLine) {
|
|
169
|
+
const subBomId = line.subBomId;
|
|
170
|
+
if (!subBomId || !loadBill) return;
|
|
171
|
+
open[line.id] = !open[line.id];
|
|
172
|
+
if (!open[line.id] || loadedBills[subBomId]?.state === 'ready') return;
|
|
173
|
+
loadedBills[subBomId] = { state: 'loading' };
|
|
174
|
+
try {
|
|
175
|
+
loadedBills[subBomId] = {
|
|
176
|
+
state: 'ready',
|
|
177
|
+
lines: await loadBill(subBomId),
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
loadedBills[subBomId] = { state: 'failed' };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function add() {
|
|
185
|
+
if (!onadd) return;
|
|
186
|
+
const result = validateBomLineInput({
|
|
187
|
+
componentSkuId: addComponent,
|
|
188
|
+
qtyPerUnit: addQty,
|
|
189
|
+
uom: addUom,
|
|
190
|
+
wastePercent: addWaste,
|
|
191
|
+
});
|
|
192
|
+
if (!result.ok) {
|
|
193
|
+
addInvalid = result.invalid;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
addInvalid = [];
|
|
197
|
+
if (await run(() => onadd(result.value))) {
|
|
198
|
+
addComponent = '';
|
|
199
|
+
addQty = '1';
|
|
200
|
+
addUom = defaultUom;
|
|
201
|
+
addWaste = '0';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function errorText(field: BomLineDraftField): string {
|
|
206
|
+
switch (field) {
|
|
207
|
+
case 'componentSkuId':
|
|
208
|
+
return t(M['manufacturing.bom_editor.error_component']);
|
|
209
|
+
case 'qtyPerUnit':
|
|
210
|
+
return t(M['manufacturing.bom_editor.error_qty']);
|
|
211
|
+
case 'uom':
|
|
212
|
+
return t(M['manufacturing.bom_editor.error_uom']);
|
|
213
|
+
default:
|
|
214
|
+
return t(M['manufacturing.bom_editor.error_waste']);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const showActions = $derived(Boolean(onupdate || onremove));
|
|
219
|
+
const ancestors = $derived(bomId ? [bomId] : []);
|
|
220
|
+
</script>
|
|
221
|
+
|
|
222
|
+
<div class="bom-editor">
|
|
223
|
+
{#if error}
|
|
224
|
+
<Alert variant="error">{error}</Alert>
|
|
225
|
+
{/if}
|
|
226
|
+
|
|
227
|
+
{#if lines.length === 0}
|
|
228
|
+
<p class="bom-editor-empty">{t(M['manufacturing.bom_editor.empty'])}</p>
|
|
229
|
+
{:else}
|
|
230
|
+
<div class="bom-editor-scroll">
|
|
231
|
+
<table class="bom-editor-table">
|
|
232
|
+
<caption class="bom-editor-caption">{t(M['manufacturing.bom_editor.caption'])}</caption>
|
|
233
|
+
<thead>
|
|
234
|
+
<tr>
|
|
235
|
+
<th scope="col">{t(M['manufacturing.bom_editor.component'])}</th>
|
|
236
|
+
<th scope="col">{t(M['manufacturing.bom_editor.kind'])}</th>
|
|
237
|
+
<th scope="col">{t(M['manufacturing.bom_editor.qty_per_unit'])}</th>
|
|
238
|
+
<th scope="col">{t(M['manufacturing.bom_editor.uom'])}</th>
|
|
239
|
+
<th scope="col">{t(M['manufacturing.bom_editor.waste_percent'])}</th>
|
|
240
|
+
{#if showActions}
|
|
241
|
+
<th scope="col">{t(M['manufacturing.bom_editor.actions'])}</th>
|
|
242
|
+
{/if}
|
|
243
|
+
</tr>
|
|
244
|
+
</thead>
|
|
245
|
+
<tbody>
|
|
246
|
+
{#each lines as line (line.id)}
|
|
247
|
+
{@const draft = draftOf(line)}
|
|
248
|
+
{@const invalid = rowInvalid[line.id] ?? []}
|
|
249
|
+
{@const canOpen =
|
|
250
|
+
line.kind === 'assembly' &&
|
|
251
|
+
line.subBomId !== null &&
|
|
252
|
+
loadBill !== undefined &&
|
|
253
|
+
!ancestors.includes(line.subBomId)}
|
|
254
|
+
{@const subId = `${uid}-sub-${line.id}`}
|
|
255
|
+
{@const errorId = `${uid}-row-error-${line.id}`}
|
|
256
|
+
<tr>
|
|
257
|
+
<th scope="row" class="bom-editor-component">{componentText(line)}</th>
|
|
258
|
+
<td>
|
|
259
|
+
<div class="bom-editor-kind">
|
|
260
|
+
<Badge size="sm" variant={line.kind === 'missing' ? 'error' : 'default'}>
|
|
261
|
+
{t(componentKindLabelKey(line.kind))}
|
|
262
|
+
</Badge>
|
|
263
|
+
{#if line.kind === 'assembly'}
|
|
264
|
+
<span class="bom-editor-note">
|
|
265
|
+
{t(
|
|
266
|
+
line.subBomId
|
|
267
|
+
? M['manufacturing.bom_editor.has_bill']
|
|
268
|
+
: M['manufacturing.bom_editor.no_bill'],
|
|
269
|
+
)}
|
|
270
|
+
</span>
|
|
271
|
+
{/if}
|
|
272
|
+
{#if canOpen}
|
|
273
|
+
<Button
|
|
274
|
+
variant="ghost"
|
|
275
|
+
size="sm"
|
|
276
|
+
aria-expanded={open[line.id] ? 'true' : 'false'}
|
|
277
|
+
aria-controls={subId}
|
|
278
|
+
aria-label={t(
|
|
279
|
+
open[line.id]
|
|
280
|
+
? M['manufacturing.bom_editor.hide_bill_aria']
|
|
281
|
+
: M['manufacturing.bom_editor.show_bill_aria'],
|
|
282
|
+
{ component: line.componentName },
|
|
283
|
+
)}
|
|
284
|
+
onclick={() => toggle(line)}
|
|
285
|
+
>
|
|
286
|
+
{t(
|
|
287
|
+
open[line.id]
|
|
288
|
+
? M['manufacturing.bom_editor.hide_bill']
|
|
289
|
+
: M['manufacturing.bom_editor.show_bill'],
|
|
290
|
+
)}
|
|
291
|
+
</Button>
|
|
292
|
+
{/if}
|
|
293
|
+
</div>
|
|
294
|
+
</td>
|
|
295
|
+
{#if onupdate}
|
|
296
|
+
<td>
|
|
297
|
+
<Input
|
|
298
|
+
type="number"
|
|
299
|
+
inputmode="decimal"
|
|
300
|
+
step="any"
|
|
301
|
+
value={draft.qtyPerUnit}
|
|
302
|
+
oninput={(event) => edit(line, 'qtyPerUnit', (event.currentTarget as HTMLInputElement).value)}
|
|
303
|
+
disabled={blocked}
|
|
304
|
+
aria-label={t(M['manufacturing.bom_editor.qty_aria'], { component: line.componentName })}
|
|
305
|
+
aria-invalid={invalid.includes('qtyPerUnit') ? 'true' : undefined}
|
|
306
|
+
aria-describedby={invalid.length > 0 ? errorId : undefined}
|
|
307
|
+
/>
|
|
308
|
+
</td>
|
|
309
|
+
<td>
|
|
310
|
+
<Input
|
|
311
|
+
type="text"
|
|
312
|
+
value={draft.uom}
|
|
313
|
+
oninput={(event) => edit(line, 'uom', (event.currentTarget as HTMLInputElement).value)}
|
|
314
|
+
disabled={blocked}
|
|
315
|
+
aria-label={t(M['manufacturing.bom_editor.uom_aria'], { component: line.componentName })}
|
|
316
|
+
aria-invalid={invalid.includes('uom') ? 'true' : undefined}
|
|
317
|
+
aria-describedby={invalid.length > 0 ? errorId : undefined}
|
|
318
|
+
/>
|
|
319
|
+
</td>
|
|
320
|
+
<td>
|
|
321
|
+
<Input
|
|
322
|
+
type="number"
|
|
323
|
+
inputmode="decimal"
|
|
324
|
+
step="any"
|
|
325
|
+
value={draft.wastePercent}
|
|
326
|
+
oninput={(event) => edit(line, 'wastePercent', (event.currentTarget as HTMLInputElement).value)}
|
|
327
|
+
disabled={blocked}
|
|
328
|
+
aria-label={t(M['manufacturing.bom_editor.waste_aria'], { component: line.componentName })}
|
|
329
|
+
aria-invalid={invalid.includes('wastePercent') ? 'true' : undefined}
|
|
330
|
+
aria-describedby={invalid.length > 0 ? errorId : undefined}
|
|
331
|
+
/>
|
|
332
|
+
</td>
|
|
333
|
+
{:else}
|
|
334
|
+
<td class="bom-editor-number">{line.qtyPerUnit}</td>
|
|
335
|
+
<td>{line.uom}</td>
|
|
336
|
+
<td class="bom-editor-number">{line.wastePercent}</td>
|
|
337
|
+
{/if}
|
|
338
|
+
{#if showActions}
|
|
339
|
+
<td>
|
|
340
|
+
<div class="bom-editor-actions">
|
|
341
|
+
{#if onupdate}
|
|
342
|
+
<Button
|
|
343
|
+
variant="secondary"
|
|
344
|
+
size="sm"
|
|
345
|
+
disabled={blocked || !isDirty(line)}
|
|
346
|
+
aria-label={t(M['manufacturing.bom_editor.save_aria'], { component: line.componentName })}
|
|
347
|
+
onclick={() => saveRow(line)}
|
|
348
|
+
>
|
|
349
|
+
{t(M['manufacturing.bom_editor.save'])}
|
|
350
|
+
</Button>
|
|
351
|
+
{/if}
|
|
352
|
+
{#if onremove}
|
|
353
|
+
<Button
|
|
354
|
+
variant="ghost"
|
|
355
|
+
size="sm"
|
|
356
|
+
disabled={blocked}
|
|
357
|
+
aria-label={t(M['manufacturing.bom_editor.remove_aria'], { component: line.componentName })}
|
|
358
|
+
onclick={() => removeRow(line)}
|
|
359
|
+
>
|
|
360
|
+
{t(M['manufacturing.bom_editor.remove'])}
|
|
361
|
+
</Button>
|
|
362
|
+
{/if}
|
|
363
|
+
</div>
|
|
364
|
+
{#if invalid.length > 0}
|
|
365
|
+
<ul class="bom-editor-errors" id={errorId}>
|
|
366
|
+
{#each invalid as field (field)}
|
|
367
|
+
<li>{errorText(field)}</li>
|
|
368
|
+
{/each}
|
|
369
|
+
</ul>
|
|
370
|
+
{/if}
|
|
371
|
+
</td>
|
|
372
|
+
{/if}
|
|
373
|
+
</tr>
|
|
374
|
+
{#if canOpen && open[line.id] && line.subBomId}
|
|
375
|
+
{@const entry = loadedBills[line.subBomId]}
|
|
376
|
+
<tr class="bom-editor-sub">
|
|
377
|
+
<td colspan={showActions ? 6 : 5}>
|
|
378
|
+
<div id={subId} class="bom-editor-sub-bill">
|
|
379
|
+
{#if entry?.state === 'ready'}
|
|
380
|
+
<BomStructureTree
|
|
381
|
+
lines={entry.lines}
|
|
382
|
+
{loadBill}
|
|
383
|
+
ancestors={[...ancestors, line.subBomId]}
|
|
384
|
+
label={t(M['manufacturing.bom_editor.sub_bill_label'], { component: line.componentName })}
|
|
385
|
+
/>
|
|
386
|
+
{:else if entry?.state === 'failed'}
|
|
387
|
+
<p class="bom-editor-note" role="alert">{t(M['manufacturing.bom_editor.load_failed'])}</p>
|
|
388
|
+
{:else}
|
|
389
|
+
<p class="bom-editor-note" role="status">{t(M['manufacturing.bom_editor.loading'])}</p>
|
|
390
|
+
{/if}
|
|
391
|
+
</div>
|
|
392
|
+
</td>
|
|
393
|
+
</tr>
|
|
394
|
+
{/if}
|
|
395
|
+
{/each}
|
|
396
|
+
</tbody>
|
|
397
|
+
</table>
|
|
398
|
+
</div>
|
|
399
|
+
{/if}
|
|
400
|
+
|
|
401
|
+
{#if onadd && components}
|
|
402
|
+
<section class="bom-editor-add" aria-labelledby="{uid}-add-heading">
|
|
403
|
+
<h3 id="{uid}-add-heading" class="bom-editor-add-heading">{t(M['manufacturing.bom_editor.add_heading'])}</h3>
|
|
404
|
+
<Form class="bom-editor-add-form" onsubmit={add}>
|
|
405
|
+
<div class="field field-component">
|
|
406
|
+
<label for="{uid}-add-component">{t(M['manufacturing.bom_editor.component'])}</label>
|
|
407
|
+
<Select
|
|
408
|
+
id="{uid}-add-component"
|
|
409
|
+
name="componentSkuId"
|
|
410
|
+
bind:value={addComponent}
|
|
411
|
+
disabled={blocked}
|
|
412
|
+
aria-invalid={addInvalid.includes('componentSkuId') ? 'true' : undefined}
|
|
413
|
+
aria-describedby={addInvalid.length > 0 ? `${uid}-add-errors` : undefined}
|
|
414
|
+
>
|
|
415
|
+
<option value="">{t(M['manufacturing.bom_editor.choose_component'])}</option>
|
|
416
|
+
{#each components as option (option.skuId)}
|
|
417
|
+
<option value={option.skuId}>{option.label}</option>
|
|
418
|
+
{/each}
|
|
419
|
+
</Select>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="field">
|
|
422
|
+
<label for="{uid}-add-qty">{t(M['manufacturing.bom_editor.qty_per_unit'])}</label>
|
|
423
|
+
<Input
|
|
424
|
+
id="{uid}-add-qty"
|
|
425
|
+
name="qtyPerUnit"
|
|
426
|
+
type="number"
|
|
427
|
+
inputmode="decimal"
|
|
428
|
+
step="any"
|
|
429
|
+
bind:value={addQty}
|
|
430
|
+
disabled={blocked}
|
|
431
|
+
aria-invalid={addInvalid.includes('qtyPerUnit') ? 'true' : undefined}
|
|
432
|
+
aria-describedby={addInvalid.length > 0 ? `${uid}-add-errors` : undefined}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="field">
|
|
436
|
+
<label for="{uid}-add-uom">{t(M['manufacturing.bom_editor.uom'])}</label>
|
|
437
|
+
<Input
|
|
438
|
+
id="{uid}-add-uom"
|
|
439
|
+
name="uom"
|
|
440
|
+
type="text"
|
|
441
|
+
bind:value={addUom}
|
|
442
|
+
disabled={blocked}
|
|
443
|
+
aria-invalid={addInvalid.includes('uom') ? 'true' : undefined}
|
|
444
|
+
aria-describedby={addInvalid.length > 0 ? `${uid}-add-errors` : undefined}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
<div class="field">
|
|
448
|
+
<label for="{uid}-add-waste">{t(M['manufacturing.bom_editor.waste_percent'])}</label>
|
|
449
|
+
<Input
|
|
450
|
+
id="{uid}-add-waste"
|
|
451
|
+
name="wastePercent"
|
|
452
|
+
type="number"
|
|
453
|
+
inputmode="decimal"
|
|
454
|
+
step="any"
|
|
455
|
+
bind:value={addWaste}
|
|
456
|
+
disabled={blocked}
|
|
457
|
+
aria-invalid={addInvalid.includes('wastePercent') ? 'true' : undefined}
|
|
458
|
+
aria-describedby={addInvalid.length > 0 ? `${uid}-add-errors` : undefined}
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="field field-submit">
|
|
462
|
+
<Button type="submit" disabled={blocked}>{t(M['manufacturing.bom_editor.add'])}</Button>
|
|
463
|
+
</div>
|
|
464
|
+
</Form>
|
|
465
|
+
{#if addInvalid.length > 0}
|
|
466
|
+
<ul class="bom-editor-errors" id="{uid}-add-errors">
|
|
467
|
+
{#each addInvalid as field (field)}
|
|
468
|
+
<li>{errorText(field)}</li>
|
|
469
|
+
{/each}
|
|
470
|
+
</ul>
|
|
471
|
+
{/if}
|
|
472
|
+
</section>
|
|
473
|
+
{/if}
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<style>
|
|
477
|
+
.bom-editor {
|
|
478
|
+
display: flex;
|
|
479
|
+
flex-direction: column;
|
|
480
|
+
gap: 1rem;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.bom-editor-scroll {
|
|
484
|
+
overflow-x: auto;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.bom-editor-table {
|
|
488
|
+
width: 100%;
|
|
489
|
+
border-collapse: collapse;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.bom-editor-caption {
|
|
493
|
+
position: absolute;
|
|
494
|
+
width: 1px;
|
|
495
|
+
height: 1px;
|
|
496
|
+
overflow: hidden;
|
|
497
|
+
clip-path: inset(50%);
|
|
498
|
+
white-space: nowrap;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.bom-editor-table th,
|
|
502
|
+
.bom-editor-table td {
|
|
503
|
+
padding: 0.5rem 0.75rem;
|
|
504
|
+
text-align: left;
|
|
505
|
+
vertical-align: top;
|
|
506
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant, transparent);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.bom-editor-table thead th {
|
|
510
|
+
font-size: var(--smrt-typography-label-medium-size, 0.75rem);
|
|
511
|
+
font-weight: var(--smrt-typography-label-medium-weight, 500);
|
|
512
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.bom-editor-component {
|
|
516
|
+
font-weight: var(--smrt-typography-title-small-weight, 500);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.bom-editor-kind,
|
|
520
|
+
.bom-editor-actions {
|
|
521
|
+
display: flex;
|
|
522
|
+
flex-wrap: wrap;
|
|
523
|
+
align-items: center;
|
|
524
|
+
gap: 0.5rem;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.bom-editor-number {
|
|
528
|
+
font-variant-numeric: tabular-nums;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.bom-editor-note,
|
|
532
|
+
.bom-editor-empty {
|
|
533
|
+
margin: 0;
|
|
534
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.bom-editor-sub-bill {
|
|
538
|
+
padding-left: 1rem;
|
|
539
|
+
border-left: 2px solid var(--smrt-color-outline-variant, transparent);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.bom-editor-errors {
|
|
543
|
+
margin: 0.25rem 0 0;
|
|
544
|
+
padding-left: 1rem;
|
|
545
|
+
color: var(--smrt-color-error, inherit);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.bom-editor-add-heading {
|
|
549
|
+
margin: 0 0 0.5rem;
|
|
550
|
+
font-size: var(--smrt-typography-title-small-size, 0.875rem);
|
|
551
|
+
font-weight: var(--smrt-typography-title-small-weight, 500);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.bom-editor-add :global(.bom-editor-add-form) {
|
|
555
|
+
display: flex;
|
|
556
|
+
flex-wrap: wrap;
|
|
557
|
+
align-items: flex-end;
|
|
558
|
+
gap: 0.75rem;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.field {
|
|
562
|
+
display: flex;
|
|
563
|
+
flex-direction: column;
|
|
564
|
+
gap: 0.25rem;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.field-component {
|
|
568
|
+
flex: 1 1 14rem;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.field label {
|
|
572
|
+
font-size: var(--smrt-typography-label-medium-size, 0.75rem);
|
|
573
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
574
|
+
}
|
|
575
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type BomComponentOption, type BomEditorLine, type BomLineDraft } from '../types.js';
|
|
2
|
+
/** What a host handler returns: `false` keeps the editor's draft. */
|
|
3
|
+
type HandlerResult = boolean | void | Promise<boolean | void>;
|
|
4
|
+
export interface BomEditorProps {
|
|
5
|
+
/** The bill's lines, in the host's order. */
|
|
6
|
+
lines: BomEditorLine[];
|
|
7
|
+
/** Components offered when adding a line; omit to hide the add form. */
|
|
8
|
+
components?: BomComponentOption[];
|
|
9
|
+
/** Adds a line. Return `false` (or reject) to keep the draft. */
|
|
10
|
+
onadd?: (draft: BomLineDraft) => HandlerResult;
|
|
11
|
+
/** Saves an edited line's quantity, unit and waste. Omit for read-only rows. */
|
|
12
|
+
onupdate?: (id: string, draft: BomLineDraft) => HandlerResult;
|
|
13
|
+
/** Removes a line. Omit to hide the remove buttons. */
|
|
14
|
+
onremove?: (id: string) => HandlerResult;
|
|
15
|
+
/** Loads a sub-assembly's bill lines by bill id, for the read-only expansion. */
|
|
16
|
+
loadBill?: (bomId: string) => Promise<BomEditorLine[]>;
|
|
17
|
+
/** The id of the bill being edited; its own bill is never expanded inside it. */
|
|
18
|
+
bomId?: string;
|
|
19
|
+
/** A message from the host, such as a refused save. */
|
|
20
|
+
error?: string | null;
|
|
21
|
+
/** Blocks every control while the host saves. */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Unit prefilled for a new line. */
|
|
24
|
+
defaultUom?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const BomEditor: import("svelte").Component<BomEditorProps, {}, "">;
|
|
27
|
+
type BomEditor = ReturnType<typeof BomEditor>;
|
|
28
|
+
export default BomEditor;
|
|
29
|
+
//# sourceMappingURL=BomEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BomEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/BomEditor.svelte.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,YAAY,EAIlB,MAAM,aAAa,CAAC;AAIrB,qEAAqE;AAErE,KAAK,aAAa,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,wEAAwE;IACxE,UAAU,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAClC,iEAAiE;IACjE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,aAAa,CAAC;IAC/C,gFAAgF;IAChF,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,KAAK,aAAa,CAAC;IAC9D,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,aAAa,CAAC;IACzC,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IACvD,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8VD,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|