@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,125 @@
1
+ <template>
2
+ <div class="inv-input" :class="{ 'inv-input--error': !!error, 'inv-input--disabled': disabled }">
3
+ <label v-if="label" :for="inputId" class="inv-input__label">
4
+ {{ label }}
5
+ </label>
6
+ <input
7
+ :id="inputId"
8
+ ref="inputRef"
9
+ class="inv-input__field"
10
+ :type="type"
11
+ :value="modelValue"
12
+ :placeholder="placeholder"
13
+ :disabled="disabled"
14
+ :aria-invalid="!!error || undefined"
15
+ :aria-describedby="error ? errorId : undefined"
16
+ @input="handleInput"
17
+ />
18
+ <p v-if="error" :id="errorId" class="inv-input__error" role="alert">
19
+ {{ error }}
20
+ </p>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { ref, onMounted, useId } from 'vue'
26
+
27
+ const props = withDefaults(defineProps<{
28
+ modelValue: string | number
29
+ label?: string
30
+ placeholder?: string
31
+ type?: 'text' | 'number' | 'search'
32
+ error?: string
33
+ disabled?: boolean
34
+ autofocus?: boolean
35
+ }>(), {
36
+ type: 'text',
37
+ disabled: false,
38
+ autofocus: false,
39
+ })
40
+
41
+ const emit = defineEmits<{
42
+ 'update:modelValue': [value: string | number]
43
+ }>()
44
+
45
+ const uid = useId()
46
+ const inputId = `inv-input-${uid}`
47
+ const errorId = `inv-input-error-${uid}`
48
+
49
+ const inputRef = ref<HTMLInputElement | null>(null)
50
+
51
+ function handleInput(event: Event) {
52
+ const target = event.target as HTMLInputElement
53
+ if (props.type === 'number') {
54
+ emit('update:modelValue', target.valueAsNumber)
55
+ } else {
56
+ emit('update:modelValue', target.value)
57
+ }
58
+ }
59
+
60
+ onMounted(() => {
61
+ if (props.autofocus && inputRef.value) {
62
+ inputRef.value.focus()
63
+ }
64
+ })
65
+ </script>
66
+
67
+ <style scoped>
68
+ .inv-input {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: var(--space-1, 0.25rem);
72
+ }
73
+
74
+ .inv-input__label {
75
+ font-size: var(--text-sm, 0.875rem);
76
+ font-weight: 500;
77
+ color: var(--admin-text-primary);
78
+ line-height: 1.4;
79
+ }
80
+
81
+ .inv-input__field {
82
+ height: 44px;
83
+ padding: 0 var(--space-3, 0.75rem);
84
+ border: 1px solid var(--admin-border);
85
+ border-radius: 6px;
86
+ background: var(--admin-card-bg);
87
+ color: var(--admin-text-primary);
88
+ font-size: var(--text-sm, 0.875rem);
89
+ line-height: 1;
90
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
91
+ outline: none;
92
+ width: 100%;
93
+ }
94
+
95
+ .inv-input__field::placeholder {
96
+ color: var(--admin-text-tertiary);
97
+ }
98
+
99
+ .inv-input__field:focus {
100
+ border-color: var(--color-primary, #2563eb);
101
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
102
+ }
103
+
104
+ .inv-input--error .inv-input__field {
105
+ border-color: var(--color-error, #dc2626);
106
+ }
107
+
108
+ .inv-input--error .inv-input__field:focus {
109
+ border-color: var(--color-error, #dc2626);
110
+ box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
111
+ }
112
+
113
+ .inv-input--disabled .inv-input__field {
114
+ opacity: 0.5;
115
+ cursor: not-allowed;
116
+ background: var(--admin-content-bg);
117
+ }
118
+
119
+ .inv-input__error {
120
+ margin: 0;
121
+ font-size: var(--text-xs, 0.75rem);
122
+ color: var(--color-error, #dc2626);
123
+ line-height: 1.4;
124
+ }
125
+ </style>
@@ -0,0 +1,296 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="inv-modal">
4
+ <div
5
+ v-if="show"
6
+ class="inv-modal__backdrop"
7
+ @mousedown.self="handleBackdropClick"
8
+ >
9
+ <div
10
+ ref="dialogRef"
11
+ class="inv-modal__dialog"
12
+ :class="[`inv-modal__dialog--${size}`]"
13
+ role="dialog"
14
+ aria-modal="true"
15
+ :aria-labelledby="titleId"
16
+ :aria-describedby="description ? descriptionId : undefined"
17
+ @keydown="handleKeydown"
18
+ >
19
+ <div class="inv-modal__header">
20
+ <div>
21
+ <h2 :id="titleId" class="inv-modal__title">{{ title }}</h2>
22
+ <p
23
+ v-if="description"
24
+ :id="descriptionId"
25
+ class="inv-modal__description"
26
+ >
27
+ {{ description }}
28
+ </p>
29
+ </div>
30
+ <button
31
+ type="button"
32
+ class="inv-modal__close"
33
+ aria-label="Close dialog"
34
+ @click="handleClose"
35
+ >
36
+ <svg
37
+ width="20"
38
+ height="20"
39
+ viewBox="0 0 20 20"
40
+ fill="none"
41
+ aria-hidden="true"
42
+ >
43
+ <path
44
+ d="M15 5L5 15M5 5L15 15"
45
+ stroke="currentColor"
46
+ stroke-width="2"
47
+ stroke-linecap="round"
48
+ stroke-linejoin="round"
49
+ />
50
+ </svg>
51
+ </button>
52
+ </div>
53
+
54
+ <div class="inv-modal__body">
55
+ <slot />
56
+ </div>
57
+
58
+ <div v-if="$slots.footer" class="inv-modal__footer">
59
+ <slot name="footer" />
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </Transition>
64
+ </Teleport>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ import { ref, watch, nextTick, onBeforeUnmount, useId } from 'vue'
69
+
70
+ const props = withDefaults(defineProps<{
71
+ show: boolean
72
+ title: string
73
+ description?: string
74
+ size?: 'sm' | 'md' | 'lg' | 'xl'
75
+ initialFocusSelector?: string
76
+ }>(), {
77
+ size: 'md',
78
+ })
79
+
80
+ const emit = defineEmits<{
81
+ 'update:show': [value: boolean]
82
+ close: []
83
+ }>()
84
+
85
+ const uid = useId()
86
+ const titleId = `inv-modal-title-${uid}`
87
+ const descriptionId = `inv-modal-desc-${uid}`
88
+
89
+ const dialogRef = ref<HTMLElement | null>(null)
90
+ let previouslyFocusedElement: HTMLElement | null = null
91
+
92
+ function getFocusableElements(): HTMLElement[] {
93
+ if (!dialogRef.value) return []
94
+ const selectors = [
95
+ 'a[href]',
96
+ 'button:not([disabled])',
97
+ 'input:not([disabled])',
98
+ 'select:not([disabled])',
99
+ 'textarea:not([disabled])',
100
+ '[tabindex]:not([tabindex="-1"])',
101
+ ]
102
+ return Array.from(dialogRef.value.querySelectorAll<HTMLElement>(selectors.join(',')))
103
+ }
104
+
105
+ function trapFocus(event: KeyboardEvent) {
106
+ const focusable = getFocusableElements()
107
+ if (focusable.length === 0) return
108
+
109
+ const first = focusable[0]
110
+ const last = focusable[focusable.length - 1]
111
+
112
+ if (event.shiftKey) {
113
+ if (document.activeElement === first) {
114
+ event.preventDefault()
115
+ last.focus()
116
+ }
117
+ } else {
118
+ if (document.activeElement === last) {
119
+ event.preventDefault()
120
+ first.focus()
121
+ }
122
+ }
123
+ }
124
+
125
+ function handleKeydown(event: KeyboardEvent) {
126
+ if (event.key === 'Escape') {
127
+ event.stopPropagation()
128
+ handleClose()
129
+ } else if (event.key === 'Tab') {
130
+ trapFocus(event)
131
+ }
132
+ }
133
+
134
+ function handleClose() {
135
+ emit('update:show', false)
136
+ emit('close')
137
+ }
138
+
139
+ function handleBackdropClick() {
140
+ handleClose()
141
+ }
142
+
143
+ watch(() => props.show, async (isVisible) => {
144
+ if (isVisible) {
145
+ previouslyFocusedElement = document.activeElement as HTMLElement
146
+ document.body.style.overflow = 'hidden'
147
+ await nextTick()
148
+ await nextTick()
149
+
150
+ if (props.initialFocusSelector && dialogRef.value) {
151
+ const target = dialogRef.value.querySelector<HTMLElement>(props.initialFocusSelector)
152
+ if (target) {
153
+ target.focus()
154
+ return
155
+ }
156
+ }
157
+
158
+ const focusable = getFocusableElements()
159
+ if (focusable.length > 0) {
160
+ focusable[0].focus()
161
+ } else {
162
+ dialogRef.value?.focus()
163
+ }
164
+ } else {
165
+ document.body.style.overflow = ''
166
+ if (previouslyFocusedElement) {
167
+ previouslyFocusedElement.focus()
168
+ previouslyFocusedElement = null
169
+ }
170
+ }
171
+ })
172
+
173
+ onBeforeUnmount(() => {
174
+ document.body.style.overflow = ''
175
+ })
176
+ </script>
177
+
178
+ <style scoped>
179
+ .inv-modal__backdrop {
180
+ position: fixed;
181
+ inset: 0;
182
+ z-index: 9999;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ padding: var(--space-4, 1rem);
187
+ background: rgba(0, 0, 0, 0.5);
188
+ backdrop-filter: blur(4px);
189
+ }
190
+
191
+ .inv-modal__dialog {
192
+ background: var(--admin-card-bg);
193
+ border: 1px solid var(--admin-border);
194
+ border-radius: 12px;
195
+ box-shadow: var(--shadow-md);
196
+ width: 100%;
197
+ max-height: calc(100vh - 2rem);
198
+ display: flex;
199
+ flex-direction: column;
200
+ outline: none;
201
+ }
202
+
203
+ .inv-modal__dialog--sm { max-width: 400px; }
204
+ .inv-modal__dialog--md { max-width: 560px; }
205
+ .inv-modal__dialog--lg { max-width: 720px; }
206
+ .inv-modal__dialog--xl { max-width: 900px; }
207
+
208
+ .inv-modal__header {
209
+ display: flex;
210
+ align-items: flex-start;
211
+ justify-content: space-between;
212
+ gap: var(--space-3, 0.75rem);
213
+ padding: var(--space-5, 1.25rem) var(--space-5, 1.25rem) var(--space-3, 0.75rem);
214
+ flex-shrink: 0;
215
+ }
216
+
217
+ .inv-modal__title {
218
+ margin: 0;
219
+ font-size: var(--text-lg, 1.125rem);
220
+ font-weight: 600;
221
+ color: var(--admin-text-primary);
222
+ line-height: 1.4;
223
+ }
224
+
225
+ .inv-modal__description {
226
+ margin: var(--space-1, 0.25rem) 0 0;
227
+ font-size: var(--text-sm, 0.875rem);
228
+ color: var(--admin-text-secondary);
229
+ line-height: 1.5;
230
+ }
231
+
232
+ .inv-modal__close {
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ width: 36px;
237
+ height: 36px;
238
+ padding: 0;
239
+ border: none;
240
+ border-radius: 6px;
241
+ background: transparent;
242
+ color: var(--admin-text-tertiary);
243
+ cursor: pointer;
244
+ flex-shrink: 0;
245
+ transition: background-color 0.15s ease, color 0.15s ease;
246
+ }
247
+
248
+ .inv-modal__close:hover {
249
+ background: var(--admin-content-bg);
250
+ color: var(--admin-text-primary);
251
+ }
252
+
253
+ .inv-modal__close:focus-visible {
254
+ outline: 2px solid var(--admin-focus-ring, #2563eb);
255
+ outline-offset: 2px;
256
+ }
257
+
258
+ .inv-modal__body {
259
+ padding: var(--space-3, 0.75rem) var(--space-5, 1.25rem);
260
+ overflow-y: auto;
261
+ flex: 1;
262
+ min-height: 0;
263
+ }
264
+
265
+ .inv-modal__footer {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: flex-end;
269
+ gap: var(--space-3, 0.75rem);
270
+ padding: var(--space-3, 0.75rem) var(--space-5, 1.25rem) var(--space-5, 1.25rem);
271
+ border-top: 1px solid var(--admin-border);
272
+ flex-shrink: 0;
273
+ }
274
+
275
+ /* Transitions */
276
+ .inv-modal-enter-active,
277
+ .inv-modal-leave-active {
278
+ transition: opacity 0.2s ease;
279
+ }
280
+
281
+ .inv-modal-enter-active .inv-modal__dialog,
282
+ .inv-modal-leave-active .inv-modal__dialog {
283
+ transition: transform 0.2s ease, opacity 0.2s ease;
284
+ }
285
+
286
+ .inv-modal-enter-from,
287
+ .inv-modal-leave-to {
288
+ opacity: 0;
289
+ }
290
+
291
+ .inv-modal-enter-from .inv-modal__dialog,
292
+ .inv-modal-leave-to .inv-modal__dialog {
293
+ transform: scale(0.95);
294
+ opacity: 0;
295
+ }
296
+ </style>
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div class="inv-select" :class="{ 'inv-select--error': !!error, 'inv-select--disabled': disabled }">
3
+ <label v-if="label" :for="selectId" class="inv-select__label">
4
+ {{ label }}
5
+ </label>
6
+ <div class="inv-select__wrapper">
7
+ <select
8
+ :id="selectId"
9
+ class="inv-select__field"
10
+ :value="modelValue"
11
+ :disabled="disabled"
12
+ :aria-invalid="!!error || undefined"
13
+ :aria-describedby="error ? errorId : undefined"
14
+ @change="handleChange"
15
+ >
16
+ <option v-if="placeholder" value="" disabled>
17
+ {{ placeholder }}
18
+ </option>
19
+ <option
20
+ v-for="opt in options"
21
+ :key="opt.value"
22
+ :value="opt.value"
23
+ >
24
+ {{ opt.label }}
25
+ </option>
26
+ </select>
27
+ <svg
28
+ class="inv-select__chevron"
29
+ width="16"
30
+ height="16"
31
+ viewBox="0 0 16 16"
32
+ fill="none"
33
+ aria-hidden="true"
34
+ >
35
+ <path
36
+ d="M4 6L8 10L12 6"
37
+ stroke="currentColor"
38
+ stroke-width="2"
39
+ stroke-linecap="round"
40
+ stroke-linejoin="round"
41
+ />
42
+ </svg>
43
+ </div>
44
+ <p v-if="error" :id="errorId" class="inv-select__error" role="alert">
45
+ {{ error }}
46
+ </p>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { useId } from 'vue'
52
+
53
+ const props = withDefaults(defineProps<{
54
+ modelValue: string | number
55
+ options: { value: string | number; label: string }[]
56
+ label?: string
57
+ placeholder?: string
58
+ error?: string
59
+ disabled?: boolean
60
+ }>(), {
61
+ disabled: false,
62
+ })
63
+
64
+ const emit = defineEmits<{
65
+ 'update:modelValue': [value: string | number]
66
+ }>()
67
+
68
+ const uid = useId()
69
+ const selectId = `inv-select-${uid}`
70
+ const errorId = `inv-select-error-${uid}`
71
+
72
+ function handleChange(event: Event) {
73
+ const target = event.target as HTMLSelectElement
74
+ const selectedOption = props.options.find(
75
+ (opt) => String(opt.value) === target.value
76
+ )
77
+ if (selectedOption) {
78
+ emit('update:modelValue', selectedOption.value)
79
+ } else {
80
+ emit('update:modelValue', target.value)
81
+ }
82
+ }
83
+ </script>
84
+
85
+ <style scoped>
86
+ .inv-select {
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: var(--space-1, 0.25rem);
90
+ }
91
+
92
+ .inv-select__label {
93
+ font-size: var(--text-sm, 0.875rem);
94
+ font-weight: 500;
95
+ color: var(--admin-text-primary);
96
+ line-height: 1.4;
97
+ }
98
+
99
+ .inv-select__wrapper {
100
+ position: relative;
101
+ display: flex;
102
+ align-items: center;
103
+ }
104
+
105
+ .inv-select__field {
106
+ height: 44px;
107
+ width: 100%;
108
+ padding: 0 var(--space-8, 2rem) 0 var(--space-3, 0.75rem);
109
+ border: 1px solid var(--admin-border);
110
+ border-radius: 6px;
111
+ background: var(--admin-card-bg);
112
+ color: var(--admin-text-primary);
113
+ font-size: var(--text-sm, 0.875rem);
114
+ line-height: 1;
115
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
116
+ outline: none;
117
+ appearance: none;
118
+ -webkit-appearance: none;
119
+ cursor: pointer;
120
+ }
121
+
122
+ .inv-select__field:focus {
123
+ border-color: var(--color-primary, #2563eb);
124
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
125
+ }
126
+
127
+ .inv-select--error .inv-select__field {
128
+ border-color: var(--color-error, #dc2626);
129
+ }
130
+
131
+ .inv-select--error .inv-select__field:focus {
132
+ border-color: var(--color-error, #dc2626);
133
+ box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
134
+ }
135
+
136
+ .inv-select--disabled .inv-select__field {
137
+ opacity: 0.5;
138
+ cursor: not-allowed;
139
+ background: var(--admin-content-bg);
140
+ }
141
+
142
+ .inv-select__chevron {
143
+ position: absolute;
144
+ right: var(--space-3, 0.75rem);
145
+ color: var(--admin-text-tertiary);
146
+ pointer-events: none;
147
+ }
148
+
149
+ .inv-select__error {
150
+ margin: 0;
151
+ font-size: var(--text-xs, 0.75rem);
152
+ color: var(--color-error, #dc2626);
153
+ line-height: 1.4;
154
+ }
155
+ </style>