@happyvertical/smrt-manufacturing 0.53.4 → 0.53.6
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 +58 -7
- package/README.md +228 -4
- package/agents/multilevel-and-runs.md +28 -0
- 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/ProductionRunCollection.d.ts +15 -0
- package/dist/collections/ProductionRunCollection.d.ts.map +1 -0
- package/dist/collections/ProductionRunCompletionCollection.d.ts +14 -0
- package/dist/collections/ProductionRunCompletionCollection.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 +5 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +23 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1660 -42
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1910 -161
- 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/ProductionRun.d.ts +67 -0
- package/dist/models/ProductionRun.d.ts.map +1 -0
- package/dist/models/ProductionRunCompletion.d.ts +36 -0
- package/dist/models/ProductionRunCompletion.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 +5 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/operation-types.d.ts +108 -0
- package/dist/operation-types.d.ts.map +1 -0
- package/dist/production-run-types.d.ts +100 -0
- package/dist/production-run-types.d.ts.map +1 -0
- package/dist/quantity.d.ts +36 -0
- package/dist/quantity.d.ts.map +1 -0
- package/dist/services/AssemblyService.d.ts +222 -0
- package/dist/services/AssemblyService.d.ts.map +1 -0
- package/dist/services/BomService.d.ts +108 -5
- 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/ProductionRunService.d.ts +123 -0
- package/dist/services/ProductionRunService.d.ts.map +1 -0
- package/dist/services/ProductionService.d.ts +11 -0
- package/dist/services/ProductionService.d.ts.map +1 -1
- package/dist/services/RoutingService.d.ts +50 -0
- package/dist/services/RoutingService.d.ts.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/structure-walk.d.ts +77 -0
- package/dist/services/structure-walk.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +1771 -207
- 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/components/ProductionRunList.svelte +213 -0
- package/dist/svelte/components/ProductionRunList.svelte.d.ts +18 -0
- package/dist/svelte/components/ProductionRunList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/RequirementsTree.svelte +125 -0
- package/dist/svelte/components/RequirementsTree.svelte.d.ts +13 -0
- package/dist/svelte/components/RequirementsTree.svelte.d.ts.map +1 -0
- package/dist/svelte/components/RequirementsTreeLevel.svelte +120 -0
- package/dist/svelte/components/RequirementsTreeLevel.svelte.d.ts +12 -0
- package/dist/svelte/components/RequirementsTreeLevel.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +119 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +119 -0
- package/dist/svelte/index.d.ts +33 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +34 -0
- package/dist/svelte/types.d.ts +257 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +249 -0
- package/dist/tenant-scope.d.ts +25 -0
- package/dist/tenant-scope.d.ts.map +1 -0
- package/dist/types.d.ts +207 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -1
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +17 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +94 -0
- package/dist/ui.js.map +1 -0
- package/package.json +41 -11
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BomStructureTree — a read-only view of one level of a bill: each line's
|
|
4
|
+
* component, kind, quantity and unit. A sub-assembly with its own active bill
|
|
5
|
+
* can be expanded to show that bill, loaded through `loadBill`, to any depth.
|
|
6
|
+
*
|
|
7
|
+
* Used by `BomEditor` to show a sub-assembly's structure without making it
|
|
8
|
+
* editable there; a sub-assembly's bill is edited on its own assembly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Badge, Button } from '@happyvertical/smrt-ui';
|
|
12
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
13
|
+
import { M } from '../i18n.js';
|
|
14
|
+
import { type BomEditorLine, componentKindLabelKey } from '../types.js';
|
|
15
|
+
import BomStructureTree from './BomStructureTree.svelte';
|
|
16
|
+
|
|
17
|
+
const { t } = useI18n();
|
|
18
|
+
|
|
19
|
+
export interface BomStructureTreeProps {
|
|
20
|
+
/** The lines of this level. */
|
|
21
|
+
lines: BomEditorLine[];
|
|
22
|
+
/** Loads a sub-assembly's bill lines by bill id. */
|
|
23
|
+
loadBill?: (bomId: string) => Promise<BomEditorLine[]>;
|
|
24
|
+
/** Bill ids already open above this level; never opened again. */
|
|
25
|
+
ancestors?: readonly string[];
|
|
26
|
+
/** Accessible name for the list. */
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
lines,
|
|
32
|
+
loadBill,
|
|
33
|
+
ancestors = [],
|
|
34
|
+
label,
|
|
35
|
+
}: BomStructureTreeProps = $props();
|
|
36
|
+
|
|
37
|
+
const uid = $props.id();
|
|
38
|
+
|
|
39
|
+
type Loaded =
|
|
40
|
+
| { state: 'loading' }
|
|
41
|
+
| { state: 'ready'; lines: BomEditorLine[] }
|
|
42
|
+
| { state: 'failed' };
|
|
43
|
+
|
|
44
|
+
let open = $state<Record<string, boolean>>({});
|
|
45
|
+
let loaded = $state<Record<string, Loaded>>({});
|
|
46
|
+
|
|
47
|
+
function componentText(line: BomEditorLine): string {
|
|
48
|
+
return line.skuCode && line.skuCode !== line.componentName
|
|
49
|
+
? `${line.componentName} (${line.skuCode})`
|
|
50
|
+
: line.componentName;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function toggle(line: BomEditorLine) {
|
|
54
|
+
const bomId = line.subBomId;
|
|
55
|
+
if (!bomId || !loadBill) return;
|
|
56
|
+
open[line.id] = !open[line.id];
|
|
57
|
+
if (!open[line.id] || loaded[bomId]?.state === 'ready') return;
|
|
58
|
+
loaded[bomId] = { state: 'loading' };
|
|
59
|
+
try {
|
|
60
|
+
loaded[bomId] = { state: 'ready', lines: await loadBill(bomId) };
|
|
61
|
+
} catch {
|
|
62
|
+
loaded[bomId] = { state: 'failed' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<ul class="bom-tree" aria-label={label}>
|
|
68
|
+
{#each lines as line (line.id)}
|
|
69
|
+
{@const canOpen =
|
|
70
|
+
line.kind === 'assembly' &&
|
|
71
|
+
line.subBomId !== null &&
|
|
72
|
+
loadBill !== undefined &&
|
|
73
|
+
!ancestors.includes(line.subBomId)}
|
|
74
|
+
{@const subId = `${uid}-${line.id}`}
|
|
75
|
+
<li class="bom-tree-line">
|
|
76
|
+
<div class="bom-tree-row">
|
|
77
|
+
<span class="bom-tree-component">{componentText(line)}</span>
|
|
78
|
+
<Badge size="sm" variant={line.kind === 'missing' ? 'error' : 'default'}>
|
|
79
|
+
{t(componentKindLabelKey(line.kind))}
|
|
80
|
+
</Badge>
|
|
81
|
+
<span class="bom-tree-qty">{line.qtyPerUnit} {line.uom}</span>
|
|
82
|
+
{#if line.kind === 'assembly' && line.subBomId === null}
|
|
83
|
+
<span class="bom-tree-note">{t(M['manufacturing.bom_editor.no_bill'])}</span>
|
|
84
|
+
{/if}
|
|
85
|
+
{#if canOpen}
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="sm"
|
|
89
|
+
aria-expanded={open[line.id] ? 'true' : 'false'}
|
|
90
|
+
aria-controls={subId}
|
|
91
|
+
aria-label={t(
|
|
92
|
+
open[line.id]
|
|
93
|
+
? M['manufacturing.bom_editor.hide_bill_aria']
|
|
94
|
+
: M['manufacturing.bom_editor.show_bill_aria'],
|
|
95
|
+
{ component: line.componentName },
|
|
96
|
+
)}
|
|
97
|
+
onclick={() => toggle(line)}
|
|
98
|
+
>
|
|
99
|
+
{t(
|
|
100
|
+
open[line.id]
|
|
101
|
+
? M['manufacturing.bom_editor.hide_bill']
|
|
102
|
+
: M['manufacturing.bom_editor.show_bill'],
|
|
103
|
+
)}
|
|
104
|
+
</Button>
|
|
105
|
+
{/if}
|
|
106
|
+
</div>
|
|
107
|
+
{#if canOpen && open[line.id] && line.subBomId}
|
|
108
|
+
{@const entry = loaded[line.subBomId]}
|
|
109
|
+
<div id={subId} class="bom-tree-children">
|
|
110
|
+
{#if entry?.state === 'ready'}
|
|
111
|
+
<BomStructureTree
|
|
112
|
+
lines={entry.lines}
|
|
113
|
+
{loadBill}
|
|
114
|
+
ancestors={[...ancestors, line.subBomId]}
|
|
115
|
+
label={t(M['manufacturing.bom_editor.sub_bill_label'], { component: line.componentName })}
|
|
116
|
+
/>
|
|
117
|
+
{:else if entry?.state === 'failed'}
|
|
118
|
+
<p class="bom-tree-note" role="alert">{t(M['manufacturing.bom_editor.load_failed'])}</p>
|
|
119
|
+
{:else}
|
|
120
|
+
<p class="bom-tree-note" role="status">{t(M['manufacturing.bom_editor.loading'])}</p>
|
|
121
|
+
{/if}
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
</li>
|
|
125
|
+
{/each}
|
|
126
|
+
</ul>
|
|
127
|
+
|
|
128
|
+
<style>
|
|
129
|
+
.bom-tree {
|
|
130
|
+
list-style: none;
|
|
131
|
+
margin: 0;
|
|
132
|
+
padding: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.bom-tree-line {
|
|
136
|
+
padding: 0.25rem 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.bom-tree-row {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-wrap: wrap;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 0.5rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.bom-tree-component {
|
|
147
|
+
font-weight: var(--smrt-typography-title-small-weight, 500);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.bom-tree-qty {
|
|
151
|
+
font-variant-numeric: tabular-nums;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.bom-tree-note {
|
|
155
|
+
margin: 0;
|
|
156
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.bom-tree-children {
|
|
160
|
+
margin-left: 1rem;
|
|
161
|
+
padding-left: 0.75rem;
|
|
162
|
+
border-left: 2px solid var(--smrt-color-outline-variant, transparent);
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BomEditorLine } from '../types.js';
|
|
2
|
+
import BomStructureTree from './BomStructureTree.svelte';
|
|
3
|
+
export interface BomStructureTreeProps {
|
|
4
|
+
/** The lines of this level. */
|
|
5
|
+
lines: BomEditorLine[];
|
|
6
|
+
/** Loads a sub-assembly's bill lines by bill id. */
|
|
7
|
+
loadBill?: (bomId: string) => Promise<BomEditorLine[]>;
|
|
8
|
+
/** Bill ids already open above this level; never opened again. */
|
|
9
|
+
ancestors?: readonly string[];
|
|
10
|
+
/** Accessible name for the list. */
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
declare const BomStructureTree: import("svelte").Component<BomStructureTreeProps, {}, "">;
|
|
14
|
+
type BomStructureTree = ReturnType<typeof BomStructureTree>;
|
|
15
|
+
export default BomStructureTree;
|
|
16
|
+
//# sourceMappingURL=BomStructureTree.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BomStructureTree.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/BomStructureTree.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,aAAa,EAAyB,MAAM,aAAa,CAAC;AACxE,OAAO,gBAAgB,MAAM,2BAA2B,CAAC;AAGzD,MAAM,WAAW,qBAAqB;IACpC,+BAA+B;IAC/B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IACvD,kEAAkE;IAClE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAwGD,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* OperationForm — add an operation, or edit one: code (new only), name,
|
|
4
|
+
* category and an optional required-qualification id. Presentational: it never
|
|
5
|
+
* persists. The host receives trimmed, checked values in `onsubmit` and calls
|
|
6
|
+
* `OperationService.define` for a new operation, or `rename` and `update` for
|
|
7
|
+
* an edit. The code is read-only when editing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Button } from '@happyvertical/smrt-ui';
|
|
11
|
+
import { Form, Input } from '@happyvertical/smrt-ui/forms';
|
|
12
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
13
|
+
import { M } from '../i18n.js';
|
|
14
|
+
import {
|
|
15
|
+
type OperationFormField,
|
|
16
|
+
type OperationFormInitial,
|
|
17
|
+
type OperationFormValues,
|
|
18
|
+
validateOperationForm,
|
|
19
|
+
} from '../types.js';
|
|
20
|
+
|
|
21
|
+
const { t } = useI18n();
|
|
22
|
+
|
|
23
|
+
export interface OperationFormProps {
|
|
24
|
+
/** The operation being edited; omit or pass null to add one. */
|
|
25
|
+
operation?: OperationFormInitial | null;
|
|
26
|
+
/** Invoked with trimmed, checked values. The form never persists. */
|
|
27
|
+
onsubmit: (values: OperationFormValues) => void;
|
|
28
|
+
/** Invoked when the form is canceled; omit to hide the cancel button. */
|
|
29
|
+
oncancel?: () => void;
|
|
30
|
+
/** Blocks input and submission while the host saves. */
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
/** Categories offered as suggestions; other values are still accepted. */
|
|
33
|
+
categories?: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
operation = null,
|
|
38
|
+
onsubmit,
|
|
39
|
+
oncancel,
|
|
40
|
+
loading = false,
|
|
41
|
+
categories = [],
|
|
42
|
+
}: OperationFormProps = $props();
|
|
43
|
+
|
|
44
|
+
const uid = $props.id();
|
|
45
|
+
const isNew = $derived(operation === null);
|
|
46
|
+
|
|
47
|
+
function getInitialFormState() {
|
|
48
|
+
return { operation };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const initialFormState = getInitialFormState();
|
|
52
|
+
let code = $state(initialFormState.operation?.code ?? '');
|
|
53
|
+
let name = $state(initialFormState.operation?.name ?? '');
|
|
54
|
+
let category = $state(initialFormState.operation?.category ?? '');
|
|
55
|
+
let requiredQualificationId = $state(
|
|
56
|
+
initialFormState.operation?.requiredQualificationId ?? '',
|
|
57
|
+
);
|
|
58
|
+
let invalid = $state<OperationFormField[]>([]);
|
|
59
|
+
let appliedOperation: OperationFormInitial | null = initialFormState.operation;
|
|
60
|
+
|
|
61
|
+
// Follow the host when it swaps the operation being edited.
|
|
62
|
+
$effect(() => {
|
|
63
|
+
if (appliedOperation === operation) return;
|
|
64
|
+
appliedOperation = operation;
|
|
65
|
+
code = operation?.code ?? '';
|
|
66
|
+
name = operation?.name ?? '';
|
|
67
|
+
category = operation?.category ?? '';
|
|
68
|
+
requiredQualificationId = operation?.requiredQualificationId ?? '';
|
|
69
|
+
invalid = [];
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function errorMessage(field: OperationFormField): string {
|
|
73
|
+
return field === 'code'
|
|
74
|
+
? t(M['manufacturing.operation_form.error_code'])
|
|
75
|
+
: t(M['manufacturing.operation_form.error_name']);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const errors = $derived(
|
|
79
|
+
invalid.map((field) => ({ field, message: errorMessage(field) })),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function errorId(field: OperationFormField): string {
|
|
83
|
+
return `${uid}-error-${field}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function describedBy(
|
|
87
|
+
field: OperationFormField,
|
|
88
|
+
hintId?: string,
|
|
89
|
+
): string | undefined {
|
|
90
|
+
const ids = [hintId, invalid.includes(field) ? errorId(field) : undefined];
|
|
91
|
+
return ids.filter(Boolean).join(' ') || undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleSubmit() {
|
|
95
|
+
const result = validateOperationForm({
|
|
96
|
+
code,
|
|
97
|
+
name,
|
|
98
|
+
category,
|
|
99
|
+
requiredQualificationId,
|
|
100
|
+
});
|
|
101
|
+
if (!result.ok) {
|
|
102
|
+
invalid = result.invalid;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
invalid = [];
|
|
106
|
+
onsubmit(result.values);
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<div class="operation-form-shell">
|
|
111
|
+
<Form class="operation-form" onsubmit={handleSubmit}>
|
|
112
|
+
<div class="field">
|
|
113
|
+
<label for="{uid}-code">
|
|
114
|
+
{t(M['manufacturing.operation_form.code'])}<span class="required" aria-hidden="true">*</span>
|
|
115
|
+
</label>
|
|
116
|
+
<Input
|
|
117
|
+
id="{uid}-code"
|
|
118
|
+
name="code"
|
|
119
|
+
type="text"
|
|
120
|
+
bind:value={code}
|
|
121
|
+
required
|
|
122
|
+
readonly={!isNew}
|
|
123
|
+
disabled={loading}
|
|
124
|
+
aria-invalid={invalid.includes('code') ? 'true' : undefined}
|
|
125
|
+
aria-describedby={describedBy('code', isNew ? undefined : `${uid}-code-hint`)}
|
|
126
|
+
/>
|
|
127
|
+
{#if !isNew}
|
|
128
|
+
<span class="hint" id="{uid}-code-hint">
|
|
129
|
+
{t(M['manufacturing.operation_form.code_edit_help'])}
|
|
130
|
+
</span>
|
|
131
|
+
{/if}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="field">
|
|
135
|
+
<label for="{uid}-name">
|
|
136
|
+
{t(M['manufacturing.operation_form.name'])}<span class="required" aria-hidden="true">*</span>
|
|
137
|
+
</label>
|
|
138
|
+
<Input
|
|
139
|
+
id="{uid}-name"
|
|
140
|
+
name="name"
|
|
141
|
+
type="text"
|
|
142
|
+
bind:value={name}
|
|
143
|
+
required
|
|
144
|
+
disabled={loading}
|
|
145
|
+
aria-invalid={invalid.includes('name') ? 'true' : undefined}
|
|
146
|
+
aria-describedby={describedBy('name')}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="field">
|
|
151
|
+
<label for="{uid}-category">{t(M['manufacturing.operation_form.category'])}</label>
|
|
152
|
+
<Input
|
|
153
|
+
id="{uid}-category"
|
|
154
|
+
name="category"
|
|
155
|
+
type="text"
|
|
156
|
+
list="{uid}-categories"
|
|
157
|
+
autocomplete="off"
|
|
158
|
+
bind:value={category}
|
|
159
|
+
disabled={loading}
|
|
160
|
+
aria-describedby="{uid}-category-hint"
|
|
161
|
+
/>
|
|
162
|
+
<datalist id="{uid}-categories">
|
|
163
|
+
{#each categories as suggestion (suggestion)}
|
|
164
|
+
<option value={suggestion}></option>
|
|
165
|
+
{/each}
|
|
166
|
+
</datalist>
|
|
167
|
+
<span class="hint" id="{uid}-category-hint">
|
|
168
|
+
{t(M['manufacturing.operation_form.category_help'])}
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="field">
|
|
173
|
+
<label for="{uid}-qualification">{t(M['manufacturing.operation_form.qualification'])}</label>
|
|
174
|
+
<Input
|
|
175
|
+
id="{uid}-qualification"
|
|
176
|
+
name="requiredQualificationId"
|
|
177
|
+
type="text"
|
|
178
|
+
bind:value={requiredQualificationId}
|
|
179
|
+
disabled={loading}
|
|
180
|
+
aria-describedby="{uid}-qualification-hint"
|
|
181
|
+
/>
|
|
182
|
+
<span class="hint" id="{uid}-qualification-hint">
|
|
183
|
+
{t(M['manufacturing.operation_form.qualification_help'])}
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{#if errors.length > 0}
|
|
188
|
+
<div class="errors" role="alert">
|
|
189
|
+
<ul>
|
|
190
|
+
{#each errors as error (error.field)}
|
|
191
|
+
<li id={errorId(error.field)}>{error.message}</li>
|
|
192
|
+
{/each}
|
|
193
|
+
</ul>
|
|
194
|
+
</div>
|
|
195
|
+
{/if}
|
|
196
|
+
|
|
197
|
+
<div class="actions">
|
|
198
|
+
{#if oncancel}
|
|
199
|
+
<Button variant="secondary" type="button" onclick={oncancel} disabled={loading}>
|
|
200
|
+
{t(M['manufacturing.operation_form.cancel'])}
|
|
201
|
+
</Button>
|
|
202
|
+
{/if}
|
|
203
|
+
<Button variant="primary" type="submit" disabled={loading}>
|
|
204
|
+
{#if loading}
|
|
205
|
+
{t(M['manufacturing.operation_form.saving'])}
|
|
206
|
+
{:else if isNew}
|
|
207
|
+
{t(M['manufacturing.operation_form.submit_add'])}
|
|
208
|
+
{:else}
|
|
209
|
+
{t(M['manufacturing.operation_form.submit_edit'])}
|
|
210
|
+
{/if}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
</Form>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<style>
|
|
217
|
+
.operation-form-shell :global(.operation-form) {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: var(--smrt-spacing-md, 1rem);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.field {
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 0.25rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
label {
|
|
230
|
+
font: var(--smrt-typography-body-medium-font, 500 0.875rem / 1.25 sans-serif);
|
|
231
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.required {
|
|
235
|
+
color: var(--smrt-color-error, inherit);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.hint {
|
|
239
|
+
font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
|
|
240
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.errors {
|
|
244
|
+
color: var(--smrt-color-error, inherit);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.errors ul {
|
|
248
|
+
margin: 0;
|
|
249
|
+
padding-left: 1.25rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.actions {
|
|
253
|
+
display: flex;
|
|
254
|
+
justify-content: flex-end;
|
|
255
|
+
gap: var(--smrt-spacing-sm, 0.5rem);
|
|
256
|
+
margin-top: var(--smrt-spacing-sm, 0.5rem);
|
|
257
|
+
}
|
|
258
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type OperationFormInitial, type OperationFormValues } from '../types.js';
|
|
2
|
+
export interface OperationFormProps {
|
|
3
|
+
/** The operation being edited; omit or pass null to add one. */
|
|
4
|
+
operation?: OperationFormInitial | null;
|
|
5
|
+
/** Invoked with trimmed, checked values. The form never persists. */
|
|
6
|
+
onsubmit: (values: OperationFormValues) => void;
|
|
7
|
+
/** Invoked when the form is canceled; omit to hide the cancel button. */
|
|
8
|
+
oncancel?: () => void;
|
|
9
|
+
/** Blocks input and submission while the host saves. */
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
/** Categories offered as suggestions; other values are still accepted. */
|
|
12
|
+
categories?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
declare const OperationForm: import("svelte").Component<OperationFormProps, {}, "">;
|
|
15
|
+
type OperationForm = ReturnType<typeof OperationForm>;
|
|
16
|
+
export default OperationForm;
|
|
17
|
+
//# sourceMappingURL=OperationForm.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OperationForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/OperationForm.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAEzB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,SAAS,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACxC,qEAAqE;IACrE,QAAQ,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChD,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAkKD,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* OperationList — a table of operations: code, name, category and status,
|
|
4
|
+
* with optional edit, retire and reinstate actions. Presentational: the host
|
|
5
|
+
* loads operations through `OperationService`, adapts them to
|
|
6
|
+
* `OperationView`, and performs the retire or reinstate it is asked for. A
|
|
7
|
+
* retired operation stays in the list so history stays readable; the host
|
|
8
|
+
* decides whether to include retired rows.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Button, StatusBadge } from '@happyvertical/smrt-ui';
|
|
12
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
13
|
+
import { M } from '../i18n.js';
|
|
14
|
+
import { type OperationView, operationStatusBadgeKey } from '../types.js';
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n();
|
|
17
|
+
|
|
18
|
+
export interface OperationListProps {
|
|
19
|
+
/** Operations to show, in the host's order. */
|
|
20
|
+
operations: OperationView[];
|
|
21
|
+
/** Invoked with the operation id when a name is activated (to edit). */
|
|
22
|
+
onselect?: (id: string) => void;
|
|
23
|
+
/** Invoked with the id to retire an active operation; omit to hide the action. */
|
|
24
|
+
onretire?: (id: string) => void;
|
|
25
|
+
/** Invoked with the id to reinstate a retired operation; omit to hide the action. */
|
|
26
|
+
onreinstate?: (id: string) => void;
|
|
27
|
+
/** Message shown when there are no operations. */
|
|
28
|
+
emptyMessage?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
operations,
|
|
33
|
+
onselect,
|
|
34
|
+
onretire,
|
|
35
|
+
onreinstate,
|
|
36
|
+
emptyMessage,
|
|
37
|
+
}: OperationListProps = $props();
|
|
38
|
+
|
|
39
|
+
const showActions = $derived(Boolean(onretire || onreinstate));
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
{#if operations.length === 0}
|
|
43
|
+
<p class="operation-list-empty">
|
|
44
|
+
{emptyMessage ?? t(M['manufacturing.operation_list.empty'])}
|
|
45
|
+
</p>
|
|
46
|
+
{:else}
|
|
47
|
+
<div class="operation-list-scroll">
|
|
48
|
+
<table class="operation-list">
|
|
49
|
+
<caption class="operation-list-caption">{t(M['manufacturing.operation_list.caption'])}</caption>
|
|
50
|
+
<thead>
|
|
51
|
+
<tr>
|
|
52
|
+
<th scope="col">{t(M['manufacturing.operation_list.code'])}</th>
|
|
53
|
+
<th scope="col">{t(M['manufacturing.operation_list.name'])}</th>
|
|
54
|
+
<th scope="col">{t(M['manufacturing.operation_list.category'])}</th>
|
|
55
|
+
<th scope="col">{t(M['manufacturing.operation_list.status'])}</th>
|
|
56
|
+
{#if showActions}
|
|
57
|
+
<th scope="col">{t(M['manufacturing.operation_list.actions'])}</th>
|
|
58
|
+
{/if}
|
|
59
|
+
</tr>
|
|
60
|
+
</thead>
|
|
61
|
+
<tbody>
|
|
62
|
+
{#each operations as operation (operation.id)}
|
|
63
|
+
<tr>
|
|
64
|
+
<th scope="row" class="operation-list-code">{operation.code}</th>
|
|
65
|
+
<td>
|
|
66
|
+
{#if onselect}
|
|
67
|
+
<Button
|
|
68
|
+
variant="ghost"
|
|
69
|
+
size="sm"
|
|
70
|
+
onclick={() => onselect(operation.id)}
|
|
71
|
+
aria-label={t(M['manufacturing.operation_list.select_aria'], { name: operation.name })}
|
|
72
|
+
>
|
|
73
|
+
{operation.name}
|
|
74
|
+
</Button>
|
|
75
|
+
{:else}
|
|
76
|
+
{operation.name}
|
|
77
|
+
{/if}
|
|
78
|
+
</td>
|
|
79
|
+
<td>
|
|
80
|
+
{#if operation.category}
|
|
81
|
+
{operation.category}
|
|
82
|
+
{:else}
|
|
83
|
+
<span class="operation-list-none">{t(M['manufacturing.operation_list.none'])}</span>
|
|
84
|
+
{/if}
|
|
85
|
+
</td>
|
|
86
|
+
<td>
|
|
87
|
+
<StatusBadge
|
|
88
|
+
status={operationStatusBadgeKey(operation.isActive)}
|
|
89
|
+
label={operation.isActive
|
|
90
|
+
? t(M['manufacturing.operation_list.active'])
|
|
91
|
+
: t(M['manufacturing.operation_list.retired'])}
|
|
92
|
+
size="sm"
|
|
93
|
+
/>
|
|
94
|
+
</td>
|
|
95
|
+
{#if showActions}
|
|
96
|
+
<td>
|
|
97
|
+
{#if operation.isActive && onretire}
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="sm"
|
|
101
|
+
onclick={() => onretire(operation.id)}
|
|
102
|
+
aria-label={t(M['manufacturing.operation_list.retire_aria'], { name: operation.name })}
|
|
103
|
+
>
|
|
104
|
+
{t(M['manufacturing.operation_list.retire'])}
|
|
105
|
+
</Button>
|
|
106
|
+
{:else if !operation.isActive && onreinstate}
|
|
107
|
+
<Button
|
|
108
|
+
variant="ghost"
|
|
109
|
+
size="sm"
|
|
110
|
+
onclick={() => onreinstate(operation.id)}
|
|
111
|
+
aria-label={t(M['manufacturing.operation_list.reinstate_aria'], { name: operation.name })}
|
|
112
|
+
>
|
|
113
|
+
{t(M['manufacturing.operation_list.reinstate'])}
|
|
114
|
+
</Button>
|
|
115
|
+
{/if}
|
|
116
|
+
</td>
|
|
117
|
+
{/if}
|
|
118
|
+
</tr>
|
|
119
|
+
{/each}
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
|
|
125
|
+
<style>
|
|
126
|
+
.operation-list-scroll {
|
|
127
|
+
overflow-x: auto;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.operation-list {
|
|
131
|
+
width: 100%;
|
|
132
|
+
border-collapse: collapse;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.operation-list-caption {
|
|
136
|
+
position: absolute;
|
|
137
|
+
width: 1px;
|
|
138
|
+
height: 1px;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
clip-path: inset(50%);
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.operation-list th,
|
|
145
|
+
.operation-list td {
|
|
146
|
+
padding: 0.5rem 0.75rem;
|
|
147
|
+
text-align: left;
|
|
148
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant, transparent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.operation-list thead th {
|
|
152
|
+
font-size: var(--smrt-typography-label-small-size, 0.75rem);
|
|
153
|
+
font-weight: var(--smrt-typography-label-small-weight, 500);
|
|
154
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.operation-list-code {
|
|
158
|
+
font-weight: var(--smrt-typography-title-small-weight, 500);
|
|
159
|
+
font-variant-numeric: tabular-nums;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.operation-list-none,
|
|
163
|
+
.operation-list-empty {
|
|
164
|
+
color: var(--smrt-color-on-surface-variant, inherit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.operation-list-empty {
|
|
168
|
+
padding: 1rem 0;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type OperationView } from '../types.js';
|
|
2
|
+
export interface OperationListProps {
|
|
3
|
+
/** Operations to show, in the host's order. */
|
|
4
|
+
operations: OperationView[];
|
|
5
|
+
/** Invoked with the operation id when a name is activated (to edit). */
|
|
6
|
+
onselect?: (id: string) => void;
|
|
7
|
+
/** Invoked with the id to retire an active operation; omit to hide the action. */
|
|
8
|
+
onretire?: (id: string) => void;
|
|
9
|
+
/** Invoked with the id to reinstate a retired operation; omit to hide the action. */
|
|
10
|
+
onreinstate?: (id: string) => void;
|
|
11
|
+
/** Message shown when there are no operations. */
|
|
12
|
+
emptyMessage?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const OperationList: import("svelte").Component<OperationListProps, {}, "">;
|
|
15
|
+
type OperationList = ReturnType<typeof OperationList>;
|
|
16
|
+
export default OperationList;
|
|
17
|
+
//# sourceMappingURL=OperationList.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OperationList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/OperationList.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,aAAa,EAA2B,MAAM,aAAa,CAAC;AAG1E,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,kFAAkF;IAClF,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qFAAqF;IACrF,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA2FD,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|