@codingfactory/inventory-locator-client 0.1.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.
Files changed (53) hide show
  1. package/package.json +47 -0
  2. package/src/api/client.ts +31 -0
  3. package/src/components/counting/CountEntryForm.vue +833 -0
  4. package/src/components/counting/CountReport.vue +385 -0
  5. package/src/components/counting/CountSessionDetail.vue +650 -0
  6. package/src/components/counting/CountSessionList.vue +683 -0
  7. package/src/components/dashboard/InventoryDashboard.vue +670 -0
  8. package/src/components/dashboard/UnlocatedProducts.vue +468 -0
  9. package/src/components/labels/LabelBatchPrint.vue +528 -0
  10. package/src/components/labels/LabelPreview.vue +293 -0
  11. package/src/components/locations/InventoryLocatorShell.vue +408 -0
  12. package/src/components/locations/LocationBreadcrumb.vue +144 -0
  13. package/src/components/locations/LocationCodeBadge.vue +46 -0
  14. package/src/components/locations/LocationDetail.vue +884 -0
  15. package/src/components/locations/LocationForm.vue +360 -0
  16. package/src/components/locations/LocationSearchInput.vue +428 -0
  17. package/src/components/locations/LocationTree.vue +156 -0
  18. package/src/components/locations/LocationTreeNode.vue +280 -0
  19. package/src/components/locations/LocationTypeIcon.vue +58 -0
  20. package/src/components/products/LocationProductAdd.vue +637 -0
  21. package/src/components/products/LocationProductList.vue +547 -0
  22. package/src/components/products/ProductLocationList.vue +215 -0
  23. package/src/components/products/QuickMoveModal.vue +592 -0
  24. package/src/components/scanning/ScanHistory.vue +146 -0
  25. package/src/components/scanning/ScanResult.vue +350 -0
  26. package/src/components/scanning/ScannerOverlay.vue +696 -0
  27. package/src/components/shared/InvBadge.vue +71 -0
  28. package/src/components/shared/InvButton.vue +206 -0
  29. package/src/components/shared/InvCard.vue +254 -0
  30. package/src/components/shared/InvEmptyState.vue +132 -0
  31. package/src/components/shared/InvInput.vue +125 -0
  32. package/src/components/shared/InvModal.vue +296 -0
  33. package/src/components/shared/InvSelect.vue +155 -0
  34. package/src/components/shared/InvTable.vue +288 -0
  35. package/src/composables/useCountSessions.ts +184 -0
  36. package/src/composables/useLabelPrinting.ts +71 -0
  37. package/src/composables/useLocationBreadcrumbs.ts +19 -0
  38. package/src/composables/useLocationProducts.ts +125 -0
  39. package/src/composables/useLocationSearch.ts +46 -0
  40. package/src/composables/useLocations.ts +159 -0
  41. package/src/composables/useMovements.ts +71 -0
  42. package/src/composables/useScanner.ts +83 -0
  43. package/src/env.d.ts +7 -0
  44. package/src/index.ts +46 -0
  45. package/src/plugin.ts +14 -0
  46. package/src/stores/countStore.ts +95 -0
  47. package/src/stores/locationStore.ts +113 -0
  48. package/src/stores/scannerStore.ts +51 -0
  49. package/src/types/index.ts +216 -0
  50. package/src/utils/codeFormatter.ts +29 -0
  51. package/src/utils/locationIcons.ts +64 -0
  52. package/tsconfig.json +21 -0
  53. package/vite.config.ts +37 -0
@@ -0,0 +1,280 @@
1
+ <template>
2
+ <div
3
+ class="tree-node"
4
+ :data-testid="`tree-node-${location.full_code}`"
5
+ >
6
+ <div
7
+ class="tree-node__row"
8
+ :class="{
9
+ 'tree-node__row--selected': selectedId === location.id,
10
+ }"
11
+ :style="{ paddingLeft: `${depth * 20}px` }"
12
+ role="treeitem"
13
+ :aria-expanded="hasChildren ? isExpanded : undefined"
14
+ :aria-selected="selectedId === location.id"
15
+ :aria-level="depth + 1"
16
+ tabindex="0"
17
+ @click="handleSelect"
18
+ @keydown="handleKeydown"
19
+ >
20
+ <!-- Expand/collapse chevron -->
21
+ <button
22
+ v-if="hasChildren"
23
+ type="button"
24
+ class="tree-node__toggle"
25
+ :aria-label="isExpanded ? `Collapse ${location.name}` : `Expand ${location.name}`"
26
+ tabindex="-1"
27
+ @click.stop="handleToggle"
28
+ >
29
+ <svg
30
+ v-if="childrenLoading"
31
+ class="tree-node__spinner"
32
+ width="14"
33
+ height="14"
34
+ viewBox="0 0 16 16"
35
+ fill="none"
36
+ aria-hidden="true"
37
+ >
38
+ <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" opacity="0.25" />
39
+ <path d="M14 8A6 6 0 0 0 8 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
40
+ </svg>
41
+ <svg
42
+ v-else
43
+ class="tree-node__chevron"
44
+ :class="{ 'tree-node__chevron--expanded': isExpanded }"
45
+ width="14"
46
+ height="14"
47
+ viewBox="0 0 16 16"
48
+ fill="none"
49
+ aria-hidden="true"
50
+ >
51
+ <path
52
+ d="M6 4L10 8L6 12"
53
+ stroke="currentColor"
54
+ stroke-width="2"
55
+ stroke-linecap="round"
56
+ stroke-linejoin="round"
57
+ />
58
+ </svg>
59
+ </button>
60
+ <span v-else class="tree-node__toggle-spacer" aria-hidden="true" />
61
+
62
+ <!-- Type icon -->
63
+ <LocationTypeIcon :type="location.type" size="sm" />
64
+
65
+ <!-- Name -->
66
+ <span class="tree-node__name">{{ location.name }}</span>
67
+
68
+ <!-- Code badge -->
69
+ <LocationCodeBadge :code="location.full_code" truncate :max-segments="2" />
70
+
71
+ <!-- Product count -->
72
+ <span
73
+ v-if="location.product_count != null && location.product_count > 0"
74
+ class="tree-node__count"
75
+ :aria-label="`${location.product_count} products`"
76
+ >
77
+ {{ location.product_count }}
78
+ </span>
79
+ </div>
80
+
81
+ <!-- Recursive children -->
82
+ <div
83
+ v-if="isExpanded && loadedChildren.length > 0"
84
+ role="group"
85
+ >
86
+ <LocationTreeNode
87
+ v-for="child in loadedChildren"
88
+ :key="child.id"
89
+ :location="child"
90
+ :depth="depth + 1"
91
+ :selected-id="selectedId"
92
+ @select="(loc) => emit('select', loc)"
93
+ @toggle="(id) => emit('toggle', id)"
94
+ />
95
+ </div>
96
+ </div>
97
+ </template>
98
+
99
+ <script setup lang="ts">
100
+ import { ref, computed, watch } from 'vue'
101
+ import type { Location } from '../../types'
102
+ import { useLocationStore } from '../../stores/locationStore'
103
+ import { useLocations } from '../../composables/useLocations'
104
+ import LocationTypeIcon from './LocationTypeIcon.vue'
105
+ import LocationCodeBadge from './LocationCodeBadge.vue'
106
+
107
+ const props = withDefaults(defineProps<{
108
+ location: Location
109
+ depth?: number
110
+ selectedId?: number | null
111
+ }>(), {
112
+ depth: 0,
113
+ selectedId: null,
114
+ })
115
+
116
+ const emit = defineEmits<{
117
+ select: [location: Location]
118
+ toggle: [id: number]
119
+ }>()
120
+
121
+ const locationStore = useLocationStore()
122
+ const { fetchChildren } = useLocations()
123
+
124
+ const childrenLoading = ref(false)
125
+ const loadedChildren = ref<Location[]>(props.location.children || [])
126
+
127
+ const hasChildren = computed(() => {
128
+ if (props.location.children && props.location.children.length > 0) return true
129
+ if (props.location.children_count && props.location.children_count > 0) return true
130
+ return false
131
+ })
132
+
133
+ const isExpanded = computed(() => locationStore.isExpanded(props.location.id))
134
+
135
+ watch(isExpanded, async (expanded) => {
136
+ if (expanded && loadedChildren.value.length === 0 && hasChildren.value) {
137
+ childrenLoading.value = true
138
+ try {
139
+ loadedChildren.value = await fetchChildren(props.location.id)
140
+ } catch {
141
+ // Error handled by composable
142
+ } finally {
143
+ childrenLoading.value = false
144
+ }
145
+ }
146
+ })
147
+
148
+ // Sync loaded children when location.children changes
149
+ watch(
150
+ () => props.location.children,
151
+ (children) => {
152
+ if (children && children.length > 0) {
153
+ loadedChildren.value = children
154
+ }
155
+ },
156
+ )
157
+
158
+ function handleSelect() {
159
+ emit('select', props.location)
160
+ }
161
+
162
+ function handleToggle() {
163
+ locationStore.toggleExpand(props.location.id)
164
+ emit('toggle', props.location.id)
165
+ }
166
+
167
+ function handleKeydown(event: KeyboardEvent) {
168
+ switch (event.key) {
169
+ case 'Enter':
170
+ case ' ':
171
+ event.preventDefault()
172
+ handleSelect()
173
+ break
174
+ case 'ArrowRight':
175
+ event.preventDefault()
176
+ if (hasChildren.value && !isExpanded.value) {
177
+ handleToggle()
178
+ }
179
+ break
180
+ case 'ArrowLeft':
181
+ event.preventDefault()
182
+ if (isExpanded.value) {
183
+ handleToggle()
184
+ }
185
+ break
186
+ }
187
+ }
188
+ </script>
189
+
190
+ <style scoped>
191
+ .tree-node__row {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: var(--space-2, 0.5rem);
195
+ padding-top: var(--space-1, 0.25rem);
196
+ padding-bottom: var(--space-1, 0.25rem);
197
+ padding-right: var(--space-3, 0.75rem);
198
+ cursor: pointer;
199
+ border-radius: 4px;
200
+ transition: background-color 0.1s ease;
201
+ user-select: none;
202
+ min-height: 36px;
203
+ }
204
+
205
+ .tree-node__row:hover {
206
+ background: var(--admin-table-row-hover);
207
+ }
208
+
209
+ .tree-node__row--selected {
210
+ background: var(--admin-sidebar-active-bg, color-mix(in srgb, var(--color-primary, #2563eb) 12%, transparent));
211
+ }
212
+
213
+ .tree-node__row--selected:hover {
214
+ background: var(--admin-sidebar-active-bg, color-mix(in srgb, var(--color-primary, #2563eb) 16%, transparent));
215
+ }
216
+
217
+ .tree-node__row:focus-visible {
218
+ outline: 2px solid var(--admin-focus-ring, #2563eb);
219
+ outline-offset: -2px;
220
+ }
221
+
222
+ .tree-node__toggle {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ width: 20px;
227
+ height: 20px;
228
+ padding: 0;
229
+ border: none;
230
+ border-radius: 4px;
231
+ background: transparent;
232
+ color: var(--admin-text-tertiary);
233
+ cursor: pointer;
234
+ flex-shrink: 0;
235
+ transition: color 0.15s ease;
236
+ }
237
+
238
+ .tree-node__toggle:hover {
239
+ color: var(--admin-text-primary);
240
+ }
241
+
242
+ .tree-node__toggle-spacer {
243
+ display: block;
244
+ width: 20px;
245
+ flex-shrink: 0;
246
+ }
247
+
248
+ .tree-node__chevron {
249
+ transition: transform 0.2s ease;
250
+ }
251
+
252
+ .tree-node__chevron--expanded {
253
+ transform: rotate(90deg);
254
+ }
255
+
256
+ .tree-node__spinner {
257
+ animation: tree-node-spin 0.8s linear infinite;
258
+ }
259
+
260
+ @keyframes tree-node-spin {
261
+ from { transform: rotate(0deg); }
262
+ to { transform: rotate(360deg); }
263
+ }
264
+
265
+ .tree-node__name {
266
+ font-size: var(--text-sm, 0.875rem);
267
+ color: var(--admin-text-primary);
268
+ white-space: nowrap;
269
+ overflow: hidden;
270
+ text-overflow: ellipsis;
271
+ min-width: 0;
272
+ }
273
+
274
+ .tree-node__count {
275
+ font-size: var(--text-xs, 0.75rem);
276
+ color: var(--admin-text-tertiary);
277
+ flex-shrink: 0;
278
+ margin-left: auto;
279
+ }
280
+ </style>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <svg
3
+ class="location-type-icon"
4
+ :class="[`location-type-icon--${size}`]"
5
+ :width="sizeMap[size]"
6
+ :height="sizeMap[size]"
7
+ :viewBox="icon.viewBox"
8
+ fill="none"
9
+ aria-hidden="true"
10
+ >
11
+ <path :d="icon.path" fill="currentColor" />
12
+ </svg>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { computed } from 'vue'
17
+ import type { LocationType } from '../../types'
18
+ import { getLocationIcon } from '../../utils/locationIcons'
19
+
20
+ const props = withDefaults(defineProps<{
21
+ type: LocationType
22
+ size?: 'sm' | 'md' | 'lg'
23
+ }>(), {
24
+ size: 'md',
25
+ })
26
+
27
+ const sizeMap: Record<string, number> = {
28
+ sm: 16,
29
+ md: 20,
30
+ lg: 24,
31
+ }
32
+
33
+ const icon = computed(() => getLocationIcon(props.type))
34
+ </script>
35
+
36
+ <style scoped>
37
+ .location-type-icon {
38
+ color: currentColor;
39
+ flex-shrink: 0;
40
+ display: inline-block;
41
+ vertical-align: middle;
42
+ }
43
+
44
+ .location-type-icon--sm {
45
+ width: 16px;
46
+ height: 16px;
47
+ }
48
+
49
+ .location-type-icon--md {
50
+ width: 20px;
51
+ height: 20px;
52
+ }
53
+
54
+ .location-type-icon--lg {
55
+ width: 24px;
56
+ height: 24px;
57
+ }
58
+ </style>