@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,146 @@
1
+ <template>
2
+ <div
3
+ v-if="displayEntries.length > 0"
4
+ class="scan-history"
5
+ >
6
+ <p class="scan-history__title">Recent scans</p>
7
+ <ul class="scan-history__list" role="list">
8
+ <li
9
+ v-for="entry in displayEntries"
10
+ :key="entry.code + entry.scannedAt"
11
+ class="scan-history__item"
12
+ >
13
+ <button
14
+ type="button"
15
+ class="scan-history__link"
16
+ :aria-label="`Navigate to ${entry.locationName} (${entry.code})`"
17
+ @click="handleClick(entry)"
18
+ >
19
+ <span class="scan-history__code">{{ entry.code }}</span>
20
+ <span class="scan-history__name">{{ entry.locationName }}</span>
21
+ <span class="scan-history__time">{{ formatTimeAgo(entry.scannedAt) }}</span>
22
+ </button>
23
+ </li>
24
+ </ul>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed, onMounted } from 'vue'
30
+ import { useRouter } from 'vue-router'
31
+ import { useScannerStore } from '../../stores/scannerStore'
32
+
33
+ const props = withDefaults(defineProps<{
34
+ limit?: number
35
+ }>(), {
36
+ limit: 10,
37
+ })
38
+
39
+ const router = useRouter()
40
+ const scannerStore = useScannerStore()
41
+
42
+ onMounted(() => {
43
+ scannerStore.loadFromSession()
44
+ })
45
+
46
+ const displayEntries = computed(() => {
47
+ return scannerStore.history.slice(0, props.limit)
48
+ })
49
+
50
+ function handleClick(entry: { code: string; locationId: number }) {
51
+ router.push({
52
+ name: 'inventory-location',
53
+ params: { code: entry.code },
54
+ })
55
+ }
56
+
57
+ function formatTimeAgo(isoDate: string): string {
58
+ const now = Date.now()
59
+ const then = new Date(isoDate).getTime()
60
+ const diffMs = now - then
61
+
62
+ const seconds = Math.floor(diffMs / 1000)
63
+ if (seconds < 60) return 'just now'
64
+
65
+ const minutes = Math.floor(seconds / 60)
66
+ if (minutes < 60) return `${minutes} min ago`
67
+
68
+ const hours = Math.floor(minutes / 60)
69
+ if (hours < 24) return `${hours}h ago`
70
+
71
+ const days = Math.floor(hours / 24)
72
+ if (days === 1) return 'yesterday'
73
+ return `${days}d ago`
74
+ }
75
+ </script>
76
+
77
+ <style scoped>
78
+ .scan-history__title {
79
+ margin: 0 0 var(--space-2, 0.5rem);
80
+ font-size: var(--text-xs, 0.75rem);
81
+ font-weight: 600;
82
+ text-transform: uppercase;
83
+ letter-spacing: 0.05em;
84
+ color: rgba(255, 255, 255, 0.5);
85
+ }
86
+
87
+ .scan-history__list {
88
+ margin: 0;
89
+ padding: 0;
90
+ list-style: none;
91
+ }
92
+
93
+ .scan-history__item {
94
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
95
+ }
96
+
97
+ .scan-history__item:last-child {
98
+ border-bottom: none;
99
+ }
100
+
101
+ .scan-history__link {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: var(--space-2, 0.5rem);
105
+ width: 100%;
106
+ padding: var(--space-2, 0.5rem) 0;
107
+ border: none;
108
+ background: none;
109
+ color: rgba(255, 255, 255, 0.8);
110
+ font-size: var(--text-sm, 0.875rem);
111
+ cursor: pointer;
112
+ text-align: left;
113
+ border-radius: 4px;
114
+ transition: color 0.15s ease;
115
+ }
116
+
117
+ .scan-history__link:hover {
118
+ color: #fff;
119
+ }
120
+
121
+ .scan-history__link:focus-visible {
122
+ outline: 2px solid var(--admin-focus-ring, #2563eb);
123
+ outline-offset: 2px;
124
+ }
125
+
126
+ .scan-history__code {
127
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
128
+ font-weight: 500;
129
+ flex-shrink: 0;
130
+ }
131
+
132
+ .scan-history__name {
133
+ overflow: hidden;
134
+ text-overflow: ellipsis;
135
+ white-space: nowrap;
136
+ min-width: 0;
137
+ flex: 1;
138
+ color: rgba(255, 255, 255, 0.5);
139
+ }
140
+
141
+ .scan-history__time {
142
+ flex-shrink: 0;
143
+ font-size: var(--text-xs, 0.75rem);
144
+ color: rgba(255, 255, 255, 0.4);
145
+ }
146
+ </style>
@@ -0,0 +1,350 @@
1
+ <template>
2
+ <Transition name="scan-result">
3
+ <div
4
+ v-if="show"
5
+ class="scan-result__backdrop"
6
+ @mousedown.self="emit('close')"
7
+ >
8
+ <div
9
+ ref="sheetRef"
10
+ class="scan-result"
11
+ role="dialog"
12
+ aria-modal="true"
13
+ :aria-labelledby="titleId"
14
+ @keydown.escape="emit('close')"
15
+ >
16
+ <!-- Drag handle -->
17
+ <div class="scan-result__handle" aria-hidden="true">
18
+ <span class="scan-result__handle-bar" />
19
+ </div>
20
+
21
+ <!-- Header -->
22
+ <div class="scan-result__header">
23
+ <svg
24
+ width="24"
25
+ height="24"
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ aria-hidden="true"
29
+ class="scan-result__check-icon"
30
+ >
31
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" />
32
+ <path d="M8 12L11 15L16 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
33
+ </svg>
34
+ <h2 :id="titleId" class="scan-result__title">Location Found</h2>
35
+ </div>
36
+
37
+ <!-- Location info -->
38
+ <div class="scan-result__body">
39
+ <!-- Path breadcrumb -->
40
+ <div v-if="location.path && location.path.length > 0" class="scan-result__path">
41
+ <LocationBreadcrumb
42
+ :path="location.path"
43
+ @navigate="handleBreadcrumbNavigate"
44
+ />
45
+ </div>
46
+
47
+ <!-- Code and type -->
48
+ <div class="scan-result__location-info">
49
+ <LocationCodeBadge :code="location.full_code" />
50
+ <InvBadge variant="muted" size="sm">
51
+ {{ location.type_label }}
52
+ </InvBadge>
53
+ </div>
54
+
55
+ <!-- Products list -->
56
+ <div
57
+ v-if="location.products && location.products.length > 0"
58
+ class="scan-result__products"
59
+ >
60
+ <p class="scan-result__products-label">
61
+ Products: {{ location.products.length }} item{{ location.products.length !== 1 ? 's' : '' }}
62
+ </p>
63
+ <ul class="scan-result__product-list" role="list">
64
+ <li
65
+ v-for="product in displayProducts"
66
+ :key="product.id"
67
+ class="scan-result__product-item"
68
+ >
69
+ <span class="scan-result__product-name">{{ product.product_name }}</span>
70
+ <span class="scan-result__product-qty">qty: {{ product.quantity }}</span>
71
+ </li>
72
+ </ul>
73
+ <p
74
+ v-if="location.products.length > 5"
75
+ class="scan-result__products-more"
76
+ >
77
+ + {{ location.products.length - 5 }} more
78
+ </p>
79
+ </div>
80
+
81
+ <div
82
+ v-else
83
+ class="scan-result__no-products"
84
+ >
85
+ <p class="scan-result__no-products-text">No products at this location</p>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Actions -->
90
+ <div class="scan-result__actions">
91
+ <InvButton
92
+ variant="primary"
93
+ size="md"
94
+ @click="emit('navigate', location)"
95
+ >
96
+ View Details
97
+ </InvButton>
98
+ <InvButton
99
+ variant="secondary"
100
+ size="md"
101
+ @click="emit('navigate', location)"
102
+ >
103
+ Add Product
104
+ </InvButton>
105
+ <InvButton
106
+ variant="ghost"
107
+ size="md"
108
+ @click="emit('scan-again')"
109
+ >
110
+ Scan Again
111
+ </InvButton>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </Transition>
116
+ </template>
117
+
118
+ <script setup lang="ts">
119
+ import { computed, watch, nextTick, ref, useId } from 'vue'
120
+ import type { Location, PathSegment } from '../../types'
121
+ import InvButton from '../shared/InvButton.vue'
122
+ import InvBadge from '../shared/InvBadge.vue'
123
+ import LocationCodeBadge from '../locations/LocationCodeBadge.vue'
124
+ import LocationBreadcrumb from '../locations/LocationBreadcrumb.vue'
125
+
126
+ const props = defineProps<{
127
+ location: Location
128
+ show: boolean
129
+ }>()
130
+
131
+ const emit = defineEmits<{
132
+ close: []
133
+ navigate: [location: Location]
134
+ 'scan-again': []
135
+ }>()
136
+
137
+ const uid = useId()
138
+ const titleId = `scan-result-title-${uid}`
139
+ const sheetRef = ref<HTMLElement | null>(null)
140
+
141
+ const displayProducts = computed(() => {
142
+ if (!props.location.products) return []
143
+ return props.location.products.slice(0, 5)
144
+ })
145
+
146
+ watch(() => props.show, async (visible) => {
147
+ if (visible) {
148
+ await nextTick()
149
+ sheetRef.value?.focus()
150
+ }
151
+ })
152
+
153
+ function handleBreadcrumbNavigate(segment: PathSegment) {
154
+ emit('navigate', { ...props.location, full_code: segment.full_code } as Location)
155
+ }
156
+ </script>
157
+
158
+ <style scoped>
159
+ .scan-result__backdrop {
160
+ position: fixed;
161
+ inset: 0;
162
+ z-index: 9100;
163
+ display: flex;
164
+ align-items: flex-end;
165
+ justify-content: center;
166
+ background: rgba(0, 0, 0, 0.3);
167
+ }
168
+
169
+ .scan-result {
170
+ width: 100%;
171
+ max-width: 560px;
172
+ max-height: 80vh;
173
+ background: var(--admin-card-bg, #fff);
174
+ border-top-left-radius: 16px;
175
+ border-top-right-radius: 16px;
176
+ box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
177
+ display: flex;
178
+ flex-direction: column;
179
+ overflow: hidden;
180
+ outline: none;
181
+ color: var(--admin-text-primary);
182
+ }
183
+
184
+ .scan-result__handle {
185
+ display: flex;
186
+ justify-content: center;
187
+ padding: var(--space-3, 0.75rem) 0 var(--space-1, 0.25rem);
188
+ }
189
+
190
+ .scan-result__handle-bar {
191
+ width: 36px;
192
+ height: 4px;
193
+ border-radius: 2px;
194
+ background: var(--admin-border);
195
+ }
196
+
197
+ .scan-result__header {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: var(--space-2, 0.5rem);
201
+ padding: var(--space-2, 0.5rem) var(--space-5, 1.25rem);
202
+ }
203
+
204
+ .scan-result__check-icon {
205
+ color: var(--color-success, #059669);
206
+ flex-shrink: 0;
207
+ }
208
+
209
+ .scan-result__title {
210
+ margin: 0;
211
+ font-size: var(--text-lg, 1.125rem);
212
+ font-weight: 600;
213
+ color: var(--admin-text-primary);
214
+ }
215
+
216
+ .scan-result__body {
217
+ padding: var(--space-2, 0.5rem) var(--space-5, 1.25rem) var(--space-4, 1rem);
218
+ overflow-y: auto;
219
+ flex: 1;
220
+ min-height: 0;
221
+ }
222
+
223
+ .scan-result__path {
224
+ margin-bottom: var(--space-3, 0.75rem);
225
+ }
226
+
227
+ .scan-result__location-info {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: var(--space-2, 0.5rem);
231
+ margin-bottom: var(--space-4, 1rem);
232
+ }
233
+
234
+ .scan-result__products {
235
+ border: 1px solid var(--admin-border);
236
+ border-radius: 8px;
237
+ overflow: hidden;
238
+ }
239
+
240
+ .scan-result__products-label {
241
+ margin: 0;
242
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
243
+ font-size: var(--text-xs, 0.75rem);
244
+ font-weight: 600;
245
+ text-transform: uppercase;
246
+ letter-spacing: 0.05em;
247
+ color: var(--admin-text-secondary);
248
+ background: var(--admin-table-header-bg);
249
+ border-bottom: 1px solid var(--admin-border);
250
+ }
251
+
252
+ .scan-result__product-list {
253
+ margin: 0;
254
+ padding: 0;
255
+ list-style: none;
256
+ }
257
+
258
+ .scan-result__product-item {
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: space-between;
262
+ gap: var(--space-2, 0.5rem);
263
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
264
+ border-bottom: 1px solid var(--admin-border);
265
+ font-size: var(--text-sm, 0.875rem);
266
+ }
267
+
268
+ .scan-result__product-item:last-child {
269
+ border-bottom: none;
270
+ }
271
+
272
+ .scan-result__product-name {
273
+ overflow: hidden;
274
+ text-overflow: ellipsis;
275
+ white-space: nowrap;
276
+ min-width: 0;
277
+ color: var(--admin-text-primary);
278
+ }
279
+
280
+ .scan-result__product-qty {
281
+ flex-shrink: 0;
282
+ font-variant-numeric: tabular-nums;
283
+ color: var(--admin-text-secondary);
284
+ }
285
+
286
+ .scan-result__products-more {
287
+ margin: 0;
288
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
289
+ font-size: var(--text-xs, 0.75rem);
290
+ color: var(--admin-text-tertiary);
291
+ border-top: 1px solid var(--admin-border);
292
+ }
293
+
294
+ .scan-result__no-products {
295
+ padding: var(--space-4, 1rem);
296
+ text-align: center;
297
+ }
298
+
299
+ .scan-result__no-products-text {
300
+ margin: 0;
301
+ font-size: var(--text-sm, 0.875rem);
302
+ color: var(--admin-text-tertiary);
303
+ font-style: italic;
304
+ }
305
+
306
+ .scan-result__actions {
307
+ display: flex;
308
+ gap: var(--space-2, 0.5rem);
309
+ padding: var(--space-3, 0.75rem) var(--space-5, 1.25rem) var(--space-5, 1.25rem);
310
+ border-top: 1px solid var(--admin-border);
311
+ flex-shrink: 0;
312
+ }
313
+
314
+ .scan-result__actions > * {
315
+ flex: 1;
316
+ }
317
+
318
+ /* Slide-up transition */
319
+ .scan-result-enter-active {
320
+ transition: opacity 0.2s ease;
321
+ }
322
+
323
+ .scan-result-enter-active .scan-result {
324
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
325
+ }
326
+
327
+ .scan-result-leave-active {
328
+ transition: opacity 0.15s ease;
329
+ }
330
+
331
+ .scan-result-leave-active .scan-result {
332
+ transition: transform 0.15s ease;
333
+ }
334
+
335
+ .scan-result-enter-from {
336
+ opacity: 0;
337
+ }
338
+
339
+ .scan-result-enter-from .scan-result {
340
+ transform: translateY(100%);
341
+ }
342
+
343
+ .scan-result-leave-to {
344
+ opacity: 0;
345
+ }
346
+
347
+ .scan-result-leave-to .scan-result {
348
+ transform: translateY(100%);
349
+ }
350
+ </style>